zxpy


Namezxpy JSON
Version 1.6.4 PyPI version JSON
download
home_pagehttps://github.com/tusharsadhwani/zxpy
SummaryShell scripts made simple
upload_time2024-09-06 22:03:04
maintainerNone
docs_urlNone
authorTushar Sadhwani
requires_python>=3.7
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # zxpy

[![Downloads](https://static.pepy.tech/badge/zxpy)](https://pepy.tech/project/zxpy)
[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![CI Status](https://github.com/tusharsadhwani/zxpy/actions/workflows/tox.yml/badge.svg)](https://github.com/tusharsadhwani/zxpy/actions/workflows/tox.yml)

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](https://github.com/google/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 `TODO`s in one of my other projects, and format them into a table:

```python
#! /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:

```console
$ ./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](./EXAMPLE.md)

## Installation <a href="https://pypi.org/project/zxpy"><img src="https://img.shields.io/badge/pypi-zxpy-blue?style=flat"></a>

```console
pip install zxpy
```

### pipx

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

```console
pipx run zxpy
```

## Basic Examples

Make a file `script.py` (The name and extension can be anything):

```python
#! /usr/bin/env zxpy
~'echo Hello world!'

file_count = ~'ls -1 | wc -l'
print("file count is:", file_count)
```

And then run it:

```console
$ 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](./examples/run_all_tests.py)

```python
#! /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:

```console
$ ./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](./EXAMPLE.md), and in the [examples folder](./examples).

## `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:

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

More examples are in the [examples folder](./examples).

## CLI Arguments

When writing a shell script, you often want to pass CLI arguments to it.

Like so:

```console
$ cat ./foo.sh
echo arg is: $1

$ ./foo.sh 123
arg is: 123
```

To do the same in `zxpy`, pass the script arguments after a `--` in the `zxpy` CLI command.

```python
#!/usr/bin/env zxpy

import sys
print("Argv is:", sys.argv)

~"echo output: $1 $2 $3"
```

```console
$ ./test.py
Argv is: ['/bin/sh']
output:

$ ./test.py -- abc def
Argv is: ['/bin/sh', 'abc', 'def']
output: abc def
```

Both `$1` and `sys.argv[1]` will do the same thing.

## Quoting

Take this shell command:

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

Now take this piece of code:

```pycon
>>> 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][1].

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

```pycon
>>> 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][1].

## Interactive mode

```pycon
$ 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:

```pycon
>>> 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:

```console
$ 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
```

[1]: https://owasp.org/www-community/attacks/Command_Injection

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/tusharsadhwani/zxpy",
    "name": "zxpy",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": null,
    "keywords": null,
    "author": "Tushar Sadhwani",
    "author_email": "tushar.sadhwani000@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/bf/b1/f55704aad4a6badda41ea717b5baec1c22610e0184b841ede4faaacfed96/zxpy-1.6.4.tar.gz",
    "platform": null,
    "description": "# zxpy\n\n[![Downloads](https://static.pepy.tech/badge/zxpy)](https://pepy.tech/project/zxpy)\n[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![CI Status](https://github.com/tusharsadhwani/zxpy/actions/workflows/tox.yml/badge.svg)](https://github.com/tusharsadhwani/zxpy/actions/workflows/tox.yml)\n\nShell scripts made simple \ud83d\udc1a\n\nzxpy lets you seamlessly write shell commands inside Python code, to create readable and maintainable shell scripts.\n\nInspired by Google's [zx](https://github.com/google/zx), but made much simpler and more accessible using Python.\n\n## Rationale\n\nBash 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.\n\n`zxpy` aims to supercharge bash by allowing you to write scripts in Python, but with native support for bash commands and pipes.\n\nLet's use it to find all `TODO`s in one of my other projects, and format them into a table:\n\n```python\n#! /usr/bin/env zxpy\ntodo_comments = ~\"git grep -n TODO\"\nfor todo in todo_comments.splitlines():\n    filename, lineno, code = todo.split(':', 2)\n    *_, comment = code.partition('TODO')\n    print(f\"{filename:40} on line {lineno:4}: {comment.lstrip(': ')}\")\n```\n\nRunning this, we get:\n\n```console\n$ ./todo_check.py\nREADME.md                                on line 154 : move this content somewhere more sensible.\ninstachat/lib/models/message.dart        on line 7   : rename to uuid\ninstachat/lib/models/update.dart         on line 13  : make int\ninstachat/lib/services/chat_service.dart on line 211 : error handling\nserver/api/api.go                        on line 94  : move these to /chat/@:address\nserver/api/user.go                       on line 80  : check for errors instead of relying on zero value\n```\n\nWriting 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.\n\n### A larger, practical example\n\nYou can find a comparison between a practical-ish script written in bash and\nzxpy in [EXAMPLE.md](./EXAMPLE.md)\n\n## Installation <a href=\"https://pypi.org/project/zxpy\"><img src=\"https://img.shields.io/badge/pypi-zxpy-blue?style=flat\"></a>\n\n```console\npip install zxpy\n```\n\n### pipx\n\nIf you have `pipx` installed, you can try out zxpy without installing it, by running:\n\n```console\npipx run zxpy\n```\n\n## Basic Examples\n\nMake a file `script.py` (The name and extension can be anything):\n\n```python\n#! /usr/bin/env zxpy\n~'echo Hello world!'\n\nfile_count = ~'ls -1 | wc -l'\nprint(\"file count is:\", file_count)\n```\n\nAnd then run it:\n\n```console\n$ chmod +x ./script.py\n\n$ ./script.py\nHello world!\nfile count is: 3\n```\n\n> Run `>>> help('zx')` in Python REPL to find out more ways to use zxpy.\n\nA slightly more involved example: [run_all_tests.py](./examples/run_all_tests.py)\n\n```python\n#! /usr/bin/env zxpy\ntest_files = (~\"find -name '*_test\\.py'\").splitlines()\n\nfor filename in test_files:\n    try:\n        print(f'Running {filename:.<50}', end='')\n        output = ~f'python {filename}'  # variables in your shell commands :D\n        assert output == ''\n        print('Test passed!')\n    except:\n        print(f'Test failed.')\n```\n\nOutput:\n\n```console\n$ ./run_all_tests.py\nRunning ./tests/python_version_test.py....................Test failed.\nRunning ./tests/platform_test.py..........................Test passed!\nRunning ./tests/imports_test.py...........................Test passed!\n```\n\nMore examples are in [EXAMPLE.md](./EXAMPLE.md), and in the [examples folder](./examples).\n\n## `stderr` and return codes\n\nTo get `stderr` and return code information out of the shell command, there is an\nalternative way of invoking the shell.\n\nTo use it, just use **3 variables** on the\nleft side of your `~'...'` shell string:\n\n```python\nstdout, stderr, return_code = ~'echo hi'\nprint(stdout)       # hi\nprint(return_code)  # 0\n```\n\nMore examples are in the [examples folder](./examples).\n\n## CLI Arguments\n\nWhen writing a shell script, you often want to pass CLI arguments to it.\n\nLike so:\n\n```console\n$ cat ./foo.sh\necho arg is: $1\n\n$ ./foo.sh 123\narg is: 123\n```\n\nTo do the same in `zxpy`, pass the script arguments after a `--` in the `zxpy` CLI command.\n\n```python\n#!/usr/bin/env zxpy\n\nimport sys\nprint(\"Argv is:\", sys.argv)\n\n~\"echo output: $1 $2 $3\"\n```\n\n```console\n$ ./test.py\nArgv is: ['/bin/sh']\noutput:\n\n$ ./test.py -- abc def\nArgv is: ['/bin/sh', 'abc', 'def']\noutput: abc def\n```\n\nBoth `$1` and `sys.argv[1]` will do the same thing.\n\n## Quoting\n\nTake this shell command:\n\n```console\n$ uname -a\nLinux pop-os 5.11.0 [...] x86_64 GNU/Linux\n```\n\nNow take this piece of code:\n\n```pycon\n>>> cmd = 'uname -a'\n>>> ~f'{cmd}'\n/bin/sh: 1: uname -a: not found\n```\n\nWhy does this not work?\n\nThis is because `uname -a` was **quoted** into `'uname -a'`. All values passed\ninside f-strings are automatically quoted to avoid [shell injection][1].\n\nTo prevent quoting, the `:raw` format_spec can be used:\n\n```pycon\n>>> cmd = 'uname -a'\n>>> ~f'{cmd:raw}'\nLinux pop-os 5.11.0 [...] x86_64 GNU/Linux\n```\n\nThis _disables_ quoting, and the command is run as-is as provided in the string.\n\n> Note that this shouldn't be used with external data, or this _will_ expose you\n> to [shell injection][1].\n\n## Interactive mode\n\n```pycon\n$ zxpy\nzxpy shell\nPython 3.8.5 (default, Jan 27 2021, 15:41:15)\n[GCC 9.3.0]\n\n>>> ~\"ls | grep '\\.py'\"\n__main__.py\nsetup.py\nzx.py\n>>>\n```\n\n> Also works with `path/to/python -m zx`\n\nIt can also be used to start a zxpy session in an already running REPL.\nSimply do:\n\n```pycon\n>>> import zx; zx.install()\n```\n\nand zxpy should be enabled in the existing session.\n\n## Development/Testing\n\nTo install from source, clone the repo, and do the following:\n\n```console\n$ source ./venv/bin/activate  # Always use a virtualenv!\n$ pip install -r requirements-dev.txt\nProcessing ./zxpy\n[...]\nSuccessfully installed zxpy-1.X.X\n$ pytest  # runs tests\n```\n\n[1]: https://owasp.org/www-community/attacks/Command_Injection\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Shell scripts made simple",
    "version": "1.6.4",
    "project_urls": {
        "Homepage": "https://github.com/tusharsadhwani/zxpy"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "fe6a389a5d202471a2241b7a203b9726ad048056c39c393ad22ec600bfa04a6d",
                "md5": "d669436c7796e6c01d28a52deb6919ea",
                "sha256": "6a3ae9b727ccdfc51829f7bc48c4dbb247957680508ed4bfa25e828e0d720eaf"
            },
            "downloads": -1,
            "filename": "zxpy-1.6.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d669436c7796e6c01d28a52deb6919ea",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 8949,
            "upload_time": "2024-09-06T22:03:02",
            "upload_time_iso_8601": "2024-09-06T22:03:02.201976Z",
            "url": "https://files.pythonhosted.org/packages/fe/6a/389a5d202471a2241b7a203b9726ad048056c39c393ad22ec600bfa04a6d/zxpy-1.6.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bfb1f55704aad4a6badda41ea717b5baec1c22610e0184b841ede4faaacfed96",
                "md5": "f2260b41ce3b03f2b81f9824ee71f724",
                "sha256": "e510f06f791a53b3462628c99438e2f4a5038c12291002b48db2ba87cd088bbb"
            },
            "downloads": -1,
            "filename": "zxpy-1.6.4.tar.gz",
            "has_sig": false,
            "md5_digest": "f2260b41ce3b03f2b81f9824ee71f724",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 8973,
            "upload_time": "2024-09-06T22:03:04",
            "upload_time_iso_8601": "2024-09-06T22:03:04.005208Z",
            "url": "https://files.pythonhosted.org/packages/bf/b1/f55704aad4a6badda41ea717b5baec1c22610e0184b841ede4faaacfed96/zxpy-1.6.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-09-06 22:03:04",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "tusharsadhwani",
    "github_project": "zxpy",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "tox": true,
    "lcname": "zxpy"
}
        
Elapsed time: 1.20421s