Skip to main content

Shell scripts made simple

Project description

zxpy

Downloads Code style: Black CI Status

Shell scripts made simple 🐚

zxpy lets you seamlessly write shell commands inside Python code, to create readable and maintainable shell scripts.

Inspired by Google's zx, but made much simpler and more accessible using Python.

Rationale

Bash 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.

zxpy aims 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 all TODOs in one of my other projects, and format them into a table:

#! /usr/bin/env zxpy
todo_comments = ~"git grep -n TODO"
for todo in todo_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.py
README.md                                on line 154 : move this content somewhere more sensible.
instachat/lib/models/message.dart        on line 7   : rename to uuid
instachat/lib/models/update.dart         on line 13  : make int
instachat/lib/services/chat_service.dart on line 211 : error handling
server/api/api.go                        on line 94  : move these to /chat/@:address
server/api/user.go                       on line 80  : check for errors instead of relying on zero value

Writing 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 example

You can find a comparison between a practical-ish script written in bash and zxpy in EXAMPLE.md

Installation

pip install zxpy

pipx

If you have pipx installed, you can try out zxpy without installing it, by running:

pipx run zxpy

Basic Examples

Make a file script.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.py
Hello world!
file count is: 3

Run >>> 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 zxpy
test_files = (~"find -name '*_test\.py'").splitlines()

for filename in test_files:
    try:
        print(f'Running {filename:.<50}', end='')
        output = ~f'python {filename}'  # variables in your shell commands :D
        assert output == ''
        print('Test passed!')
    except:
        print(f'Test failed.')

Output:

$ ./run_all_tests.py
Running ./tests/python_version_test.py....................Test failed.
Running ./tests/platform_test.py..........................Test passed!
Running ./tests/imports_test.py...........................Test passed!

More examples are in EXAMPLE.md, and in the examples folder.

stderr and return codes

To get stderr and return code information out of the shell command, there is an alternative way of invoking the shell.

To use it, just use 3 variables on the left side of your ~'...' shell string:

stdout, stderr, return_code = ~'echo hi'
print(stdout)       # hi
print(return_code)  # 0

More examples are in the examples folder.

Quoting

Take this shell command:

$ uname -a
Linux pop-os 5.11.0 [...] x86_64 GNU/Linux

Now take this piece of code:

>>> cmd = 'uname -a'
>>> ~f'{cmd}'
/bin/sh: 1: uname -a: not found

Why does this not work?

This is because uname -a was quoted into 'uname -a'. All values passed inside f-strings are automatically quoted to avoid shell injection.

To prevent quoting, the :raw format_spec can be used:

>>> cmd = 'uname -a'
>>> ~f'{cmd:raw}'
Linux pop-os 5.11.0 [...] x86_64 GNU/Linux

This disables quoting, and the command is run as-is as provided in the string.

Note that this shouldn't be used with external data, or this will expose you to shell injection.

Interactive mode

$ zxpy
zxpy shell
Python 3.8.5 (default, Jan 27 2021, 15:41:15)
[GCC 9.3.0]

>>> ~"ls | grep '\.py'"
__main__.py
setup.py
zx.py
>>>

Also works with path/to/python -m zx

It can also be used to start a zxpy session in an already running REPL. Simply do:

>>> import zx; zx.install()

and zxpy should be enabled in the existing session.

Development/Testing

To install from source, clone the repo, and do the following:

$ source ./venv/bin/activate  # Always use a virtualenv!
$ pip install -r requirements-dev.txt
Processing ./zxpy
[...]
Successfully installed zxpy-1.X.X
$ pytest  # runs tests

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zxpy-1.6.0.tar.gz (8.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zxpy-1.6.0-py3-none-any.whl (8.1 kB view details)

Uploaded Python 3

File details

Details for the file zxpy-1.6.0.tar.gz.

File metadata

  • Download URL: zxpy-1.6.0.tar.gz
  • Upload date:
  • Size: 8.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/52.0.0 requests-toolbelt/0.9.1 tqdm/4.58.0 CPython/3.9.7

File hashes

Hashes for zxpy-1.6.0.tar.gz
Algorithm Hash digest
SHA256 48a7215f65c72c18e20f8d909da294af1e3f4017bc7ccff2cb4d1fc3150c5e21
MD5 948f2bf8ca6e3992a7f2fc9bbc583dac
BLAKE2b-256 076b8ee3284b57684e54c8d6dc80e14dc3ade31089764eb1d7eec1d4b23237f7

See more details on using hashes here.

File details

Details for the file zxpy-1.6.0-py3-none-any.whl.

File metadata

  • Download URL: zxpy-1.6.0-py3-none-any.whl
  • Upload date:
  • Size: 8.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/52.0.0 requests-toolbelt/0.9.1 tqdm/4.58.0 CPython/3.9.7

File hashes

Hashes for zxpy-1.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a6f24671e4709880e5e5eb420638997b5df12adaed4d9ce4a5ccb87d8e1db078
MD5 9cace66e63c7e2381794ba0f51b77f4d
BLAKE2b-256 5f6e84c81c387c2e3190abe8f5677cb1f2131ac9b91ad766a718512b30182099

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page