# zxpy
[](https://pepy.tech/project/zxpy)
[](https://github.com/psf/black)
[](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[](https://pepy.tech/project/zxpy)\n[](https://github.com/psf/black)\n[](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"
}