Package: shellrunner
ShellRunnerWrite safe shell scripts in Python.Combine the streamlined utility of a shell with the power of a modern programming language.InstallUsageWhy?Similar ProjectsAdvanced UsageShell Command ResultException HandlingMultiple Commands / Persisting EnvironmentOptionsOutputEnvironment VariablesExamplesInstallpip install -U shellrunnerUsagefromshellrunnerimportXX("echo hello world")# hello worldEasily get a command's output, do something with it, and run another command using the value:output=X("echo hello world | sed 's/world/there/'").outgreeting=output.capitalize()X(f"echo 'echo{greeting}' >> .bashrc")An exception is raised if a command exits with a non-zero status (like bash'sset -e):text=X("grep hello /non/existent/file").out# grep exits with a non-zero status# ^ Raises ShellCommandError so the rest of the script doesn't runmy_text_processor(text)Or, maybe you want to handle the error:fromshellrunnerimportX,ShellCommandErrortext=""try:text=X("grep hello /non/existent/file").outexceptShellCommandError:text=X("grep hello /file/that/definitely/exists").outmy_text_processor(text)Pipeline errors are not masked (like bash'sset -o pipefail):X("grep hello /non/existent/file | tee new_file")# tee gets nothing from grep, creates an empty file, and exits with status 0# ^ Raises ShellCommandErrorWhy?Why not just use bash withset -eandset -o pipefail?Because writing anything remotely complicated in bash kinda sucks :)One of the primary advantages of ShellRunner's approach is that you can seamlessly swap between the shell and Python. Some things are just easier to do in a shell (e.g. pipelines) and a lot of things are easier/better in Python (control flow, error handling, etc).Also, users offishmight know that itdoes not offer a way to easily exit a script if a command fails. ShellRunner addsset -eandpipefaillike functionality to any shell. Leverage the improved syntax of your preferred shell and the (optional) safety of bash.Similar ProjectszxpyshellpyplumbumShellRunner is very similar to zxpy and shellpy but aims to be more simple in its implementation and has a focus on adding safety to scripts.Advanced UsageA note on compatibility: ShellRunner should work with on any POSIX-compliant system (and shell). No Windows support at this time.Confirmed compatible withsh(dash),bash,zsh, andfish.Commands are automatically run with the shell that invoked your python script (this can beoverridden):# my_script.pyX("echo hello | string match hello")# Works if my_script.py is executed under fish (string match). Will obviously fail if using bash.Shell Command ResultXreturns aShellCommandResult(NamedTuple) containing the following:out: str: Thestdoutandstderrof the command.status: int: The overall exit status of the command. If the command was a pipeline that failed,statuswill be equal to the status of the last failing command (like bash'spipefail).pipestatus: list[int]: A list of statuses for each command in the pipeline.result=X("echo hello")print(f'Got output "{result.out}" with exit status{result.status}/{result.pipestatus}')# Or unpackoutput,status,pipestatus=X("echo hello")# output = "hello"# status = 0# pipestatus = [0]result=X("(exit 1) | (exit 2) | echo hello")# result.out = "hello"# result.status = 2# result.pipestatus = [1, 2, 0]If using a shell that does not supportPIPESTATUSsuch assh, you will only ever get the status of the last command in a pipeline.This also means that in this case ShellRunner cannot detect if an error occurred in a pipeline:result=X("(exit 1) | echo hello")# if invoked with bash: ShellCommandError is raised, status = 1, pipestatus = [1, 0]# if invoked with sh: No exception is raised, status = 0, pipestatus = [0]Exception HandlingShellCommandErroralso receives the information from the failed command, which means you can do something like this:try:X("echo hello && false")# Pretend this is some command that prints something but exits with a non-zero statusexceptShellCommandErrorase:print(f'Command failed. Got output "{e.out}" with exit status{e.status}')Multiple Commands / Persisting EnvironmentEach call ofXinvokes a new instance of the shell, so things like environment variables or directory changes don't persist.Sometimes you might want to do something like this:X("MY_VAR=hello")X("grep $MY_VAR /file/that/exists")# MY_VAR doesn't exist# ^ Raises ShellCommandErrorA (bad) solution would be to do this:X("MY_VAR=hello; grep $MY_VAR /file/that/exists")This sort of defeats the purpose of ShellRunner because that would be run as one command, so no error handling can take place on commands before the last one.Instead,Xalso accepts a list of commands where each command is run in the same shell instance and goes through the normal error handling:X(["MY_VAR=hello","grep $MY_VAR /file/that/exists",])# Works!OptionsThere are a few keyword arguments you can provide to adjust the behavior ofX:X("command",shell="bash",check=True,show_output=True,show_command=True)shell: str(Default: the invoking shell) - Shell that will be used to execute the commands. Can be a path or simply the name (e.g. "/bin/bash", "bash").check: bool(Default: True) - If True, an error will be thrown if a command exits with a non-zero status.show_output: bool(Default: True) - If True, command output will be printed.show_command: bool(Default: True) - If True, the current command will be printed before execution.OutputSay you do this:X("echo hello world")This will print the following to your terminal:shellrunner: echo hello world
hello worldTo hide theshellrunner:lines, setshow_command=False.To hide actual command output, setshow_output=False.Environment VariablesEach option also has a corresponding environment variable to allow you to set these options "globally" for your script:shell=SHELLRUNNER_SHELLcheck=SHELLRUNNER_CHECKshow_output=SHELLRUNNER_SHOW_OUTPUTshow_command=SHELLRUNNER_SHOW_COMMANDEnvironment variables are evaluated on each call ofX, so you could also do something like this:# Pretend that before running this file you set: export SHELLRUNNER_SHOW_OUTPUT="False"X("echo hello")# No output# Now you want to see outputos.environ["SHELLRUNNER_SHOW_OUTPUT"]="True"X("echo hello")# helloExamplesPrints out installed python packages and their dependencies:fromshellrunnerimportXpackages=X("pip list -l | sed 1,2d | awk '{print $1}'").outpackages=packages.splitlines()forpackageinpackages:print(f"==={package}===")X(f"pip show{package}| grep -E 'Requires|Required-by'",show_command=False)