Package: shpyx
shpyxis a simple, lightweight and typed library for running shell commands in Python.Useshpyx.runto run a shell command in a subprocess:>>>importshpyx>>>shpyx.run("echo 1").return_code0>>>shpyx.run("echo 1").stdout'1\n'>>>shpyx.run("echo 1").stderr''>>>shpyx.run("echo 1")ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)InstallationInstall withpip:pipinstallshpyxHow TosRun a commandIn string format:>>>shpyx.run("echo 1")ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)In list format:>>>shpyx.run(["echo",["1"])ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)Run a command and print live output>>>shpyx.run("echo 1",log_output=True)1ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)Run a command with shell specific logicWhen the argument torunis a string, an actual shell is created in the subprocess and shell logic can be used.
For example, the pipe operator can be used in bash/sh:>>>shpyx.run("seq 1 5 | grep '2'")ShellCmdResult(cmd="seq 1 5 | grep '2'",stdout='2\n',stderr='',all_output='2\n',return_code=0)Create a custom runnerUse a custom runner to override the execution defaults, and not have to pass them to every call.For example, we can change the default value oflog_cmd, so that all commands are logged:>>>shpyx.run("echo 1")ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)>>>shpyx.run("echo 1",log_cmd=True)Running:echo1ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)>>>runner=shpyx.Runner(log_cmd=True)>>>runner.run("echo 1")Running:echo1ShellCmdResult(cmd='echo 1',stdout='1\n',stderr='',all_output='1\n',return_code=0)Propagating terminal control sequencesNote: as of now this is only supported for Unix environments.Some commands, likepsql, might output special characters used for terminal management like cursor movement and
colors. For example, thepsqlcommand is used to start an interactive shell against a Postgres DB:shpyx.run(f"psql -h{host}-p{port}-U{user}-d{database}",log_output=True)However, the above call will not work as good as runningpsqldirectly, due to terminal control sequences not being
properly propagated. To make it work well, we need to use thescriptutility which will properly propagate all control sequences:# Linux:shpyx.run(f"script -q -c 'psql -h{host}-p{port}-U{user}-d{database}'",log_output=True)# MacOS:shpyx.run(f"script -q psql -h{host}-p{port}-U{user}-d{database}",log_output=True)shpyx provides a keyword argument that does this wrapping automatically,unix_raw:shpyx.run(f"psql -h{host}-p{port}-U{user}-d{database}",log_output=True,unix_raw=True)The flag is disabled by default, and should only be used for interactive commands likepsql.API ReferenceThe following arguments are supported byRunner:NameDescriptionDefaultlog_cmdLog the executed command.Falselog_outputLog the live output of the command (while it is being executed).Falseverify_return_codeRaise an exception if the shell return code of the command is not0.Trueverify_stderrRaise an exception if anything was written to stderr during the execution.Falseuse_signal_namesLog the name of the signal corresponding to a non-zero error code.TrueThe following arguments are supported byrun:NameDescriptionDefaultlog_cmdLog the executed command.Runner defaultlog_outputLog the live output of the command (while it is being executed).Runner defaultverify_return_codeRaise an exception if the shell return code of the command is not0.Runner defaultverify_stderrRaise an exception if anything was written to stderr during the execution.Runner defaultuse_signal_namesLog the name of the signal corresponding to a non-zero error code.Runner defaultenvEnvironment variables to set during the execution of the command.Same as parent processexec_dirCustom path to execute the command in (defaults to current directory).Same as parent processunix_raw(UNIX ONLY) Whether to use thescriptUnix utility to run the command.FalseImplementation detailsshpyxis a wrapper around the excellentsubprocessmodule, aiming
to concentrate all the different API functions (Popen/communicate/poll/wait) into a single function -shpyx.run.While the core API logic is fully supported on both Unix and Windows systems, there is some OS specific code for minor quality-of-life
improvements.
For example, on non Windows systems,fcntlis used to configure the subprocess to
always be incorruptible (which means one can CTRL-C out of any command).SecurityThe call tosubprocess.Popenusesshell=Truewhen the input torunis a string (to support shell logic like bash piping).
This means that an actual system shell is being created, and the subprocess has the permissions of the main Python process.It is therefore recommended not pass any untrusted input toshpyx.run.For more info, seesecurity considerations.Useful linksRelevant Python libraries:subprocessshlexOther user libraries for running shell commands in Python:sargeshContributingTo contribute simply open a PR with your changes.Tests, linters and type checks are run in CI through GitHub Actions.Running checks locallyTo run checks locally, start by installing all the development dependencies:poetryinstallTo run the linters usepre-commit:pre-commitrun-aTo run the unit tests usepytest:pytest-ctests/pytest.initestsTo run type checks usemypy:mypy--config-fileshpyx/mypy.tomlshpyxtestsTo trigger a deployment of a new version upon merge, bump the version number inpyproject.toml.