Package: zxpy
zxpyShell scripts made simple 🐚zxpy lets you seamlessly write shell commands inside Python code, to create readable and maintainable shell scripts.Inspired by Google'szx, but made much simpler and more accessible using Python.RationaleBash is cool, and it's extremely powerful when paired with linux coreutils and pipes. But apart from that, it's a whole another language to learn, and has a (comparatively) unintuitive syntax for things like conditionals and loops.zxpyaims to supercharge bash by allowing you to write scripts in Python, but with native support for bash commands and pipes.Let's use it to find allTODOs in one of my other projects, and format them into a table:#! /usr/bin/env zxpytodo_comments=~"git grep -n TODO"fortodointodo_comments.splitlines():filename,lineno,code=todo.split(':',2)*_,comment=code.partition('TODO')print(f"{filename:40}on line{lineno:4}:{comment.lstrip(': ')}")Running this, we get:$./todo_check.pyREADME.md                                on line 154 : move this content somewhere more sensible.instachat/lib/models/message.dart        on line 7   : rename to uuidinstachat/lib/models/update.dart         on line 13  : make intinstachat/lib/services/chat_service.dart on line 211 : error handlingserver/api/api.go                        on line 94  : move these to /chat/@:addressserver/api/user.go                       on line 80  : check for errors instead of relying on zero valueWriting something like this purely in bash or in Python would be much harder than this. Being able to use linux utilities seamlessly with a readable, general purpose language is what makes this a really powerful tool.A larger, practical exampleYou can find a comparison between a practical-ish script written in bash and
zxpy inEXAMPLE.mdInstallationpip install zxpypipxIf you havepipxinstalled, you can try out zxpy without installing it, by running:pipx run zxpyBasic ExamplesMake a filescript.py(The name and extension can be anything):#! /usr/bin/env zxpy~'echo Hello world!'file_count=~'ls -1 | wc -l'print("file count is:",file_count)And then run it:$chmod+x./script.py$./script.pyHello world!file count is: 3Run>>> help('zx')in Python REPL to find out more ways to use zxpy.A slightly more involved example:run_all_tests.py#! /usr/bin/env zxpytest_files=(~"find -name '*_test\.py'").splitlines()forfilenameintest_files:try:print(f'Running{filename:.<50}',end='')output=~f'python{filename}'# variables in your shell commands :Dassertoutput==''print('Test passed!')except:print(f'Test failed.')Output:$./run_all_tests.py
Running./tests/python_version_test.py....................Testfailed.
Running./tests/platform_test.py..........................Testpassed!
Running./tests/imports_test.py...........................Testpassed!More examples are inEXAMPLE.md, and in theexamples folder.stderrand return codesTo getstderrand return code information out of the shell command, there is an
alternative way of invoking the shell.To use it, just use3 variableson the
left side of your~'...'shell string:stdout,stderr,return_code=~'echo hi'print(stdout)# hiprint(return_code)# 0More examples are in theexamples folder.CLI ArgumentsWhen writing a shell script, you often want to pass CLI arguments to it.Like so:$cat./foo.shecho arg is: $1$./foo.sh123arg is: 123To do the same inzxpy, pass the script arguments after a--in thezxpyCLI command.#!/usr/bin/env zxpyimportsysprint("Argv is:",sys.argv)~"echo output: $1 $2 $3"$./test.pyArgv is: ['/bin/sh']output:$./test.py--abcdefArgv is: ['/bin/sh', 'abc', 'def']output: abc defBoth$1andsys.argv[1]will do the same thing.QuotingTake this shell command:$uname-aLinux pop-os 5.11.0 [...] x86_64 GNU/LinuxNow take this piece of code:>>>cmd='uname -a'>>>~f'{cmd}'/bin/sh: 1: uname -a: not foundWhy does this not work?This is becauseuname -awasquotedinto'uname -a'. All values passed
inside f-strings are automatically quoted to avoidshell injection.To prevent quoting, the:rawformat_spec can be used:>>>cmd='uname -a'>>>~f'{cmd:raw}'Linux pop-os 5.11.0 [...] x86_64 GNU/LinuxThisdisablesquoting, and the command is run as-is as provided in the string.Note that this shouldn't be used with external data, or thiswillexpose you
toshell injection.Interactive mode$ zxpyzxpy shellPython 3.8.5 (default, Jan 27 2021, 15:41:15)[GCC 9.3.0]>>>~"ls | grep '\.py'"__main__.pysetup.pyzx.py>>>Also works withpath/to/python -m zxIt can also be used to start a zxpy session in an already running REPL.
Simply do:>>>importzx;zx.install()and zxpy should be enabled in the existing session.Development/TestingTo install from source, clone the repo, and do the following:$source./venv/bin/activate# Always use a virtualenv!$pipinstall-rrequirements-dev.txtProcessing ./zxpy[...]Successfully installed zxpy-1.X.X$pytest# runs tests