PyBash
Write shell commands directly in Python. PyBash rewrites lines that start with $ or > into
subprocess calls, so you get the brevity of a shell script with the rest of Python around it.
branch = ($git rev-parse --abbrev-ref HEAD).decode().strip()
$git fetch origin && git rebase origin/{{branch}} || echo "rebase failed"
count = $git log --oneline | wc -l
print(f"{branch}: {int(count)} commits")
$commands never go through a shell. Arguments are split at transform time, and pipes, redirects and&&/||/;are run by PyBash itself, so a value can never turn into shell syntax.>commands are handed to the shell as written, for globbing and expansion. Interpolated values are quoted for you.- One line in, one line out: tracebacks point at the line you wrote.
- No dependencies. Python 3.11+.
Install
pip install pybash
Use
Run a script
pybash script.py [args...] # or: python -m pybash script.py
pybash --show script.py # print the generated Python instead of running it
pybash -c '$echo hello'
Modules the script imports from its own project may use the syntax too.
Transform source yourself
from pybash import transform
transform("$echo hello world") # 'subprocess.run(["echo", "hello", "world"])'
Plain commands compile to bare subprocess calls. Commands with operators call a small helper in
pybash.runtime; by default the generated code imports it in place, so the output only needs
import subprocess. For tidier output, name the helper yourself and add the header:
import pybash
body = pybash.transform(source, runtime=pybash.RUNTIME_NAME)
code = pybash.header() + body # imports pybash.runtime
code = pybash.header(standalone=True) + body # pastes the helper in: no pybash needed at run time
pybash.compile_source(source, filename) returns a code object ready for exec, and
pybash.add_hook() installs an import hook for the current process.
Mistakes raise PybashSyntaxError, a SyntaxError whose filename, lineno, offset and text
point at the original line.
Syntax
Run: $command
$python --version
$cp test.txt test_copy.txt
Returns the subprocess.CompletedProcess. A non-zero exit does not raise.
Capture: x = $command, ($command), $(command)
out = $cat test.txt
print(($cat test.txt).decode().strip())
lines = $(wc -l test.txt).decode().split()[0]
Captures are bytes, and raise subprocess.CalledProcessError on a non-zero exit.
A capture not wrapped in brackets runs to the end of the line.
Operators
| Operator | Meaning |
|---|---|
a | b |
pipe stdout of a into b |
a |& b |
pipe stdout and stderr |
a && b, a || b, a ; b |
run b if a succeeded / failed / either way |
< file |
read stdin from a file |
> file, >> file |
write / append stdout |
2> file, 2>> file |
write / append stderr |
&> file, &>> file |
write / append both |
2>&1, >&2 |
send stderr to stdout / stdout to stderr |
$sort < in.txt | uniq -c | sort -rn > counts.txt
$make build > build.log 2>&1 && echo ok || echo failed
newest = $ls -t | head -n 1
Operators work without surrounding spaces (a|b, 2>err.log). Quote or escape one to pass it as
text: $echo 'a && b' \| c. Several output files on one command all receive the output
($echo hi > a.txt >> b.txt).
Background jobs (&), here-documents (<<) and other file descriptors are not supported in $
commands; use a > shell command for those.
Quoting
$ commands follow POSIX shell quoting: single quotes are literal, double quotes allow \" and \\,
and a backslash escapes the next character. An unquoted # at the start of a word begins a comment.
There is no globbing, ~ or $VAR expansion; use > when you need the shell.
A trailing backslash continues the command on the next line.
Interpolation
| Form | Meaning |
|---|---|
{{ expr }} |
the value of any Python expression, converted with str() |
f{ expr } |
the same, with f-string formatting available: f{n:03d}, f{name!r} |
$git {{command}} {{get_option(command)}}
$kubectl get pods --show-{{display_type}}=true -n f{namespace}
$echo "{{user}} has f{count:,} files"
In a $ command an interpolated value is always exactly one argument, whatever it contains.
Interpolation also applies inside quotes. To write the markers literally, escape them: \{{ and \f{.
Shell: >command
>ls .github/* | wc -l
>echo "hello {{name}}" > out.txt
The line is passed to the shell untouched. Interpolated values are quoted for where they appear
(bare, inside "..." or inside '...'), so they are always data. To splice shell syntax in on
purpose, use {{! expr }}:
pattern = "*.txt"
>ls {{! pattern }}
Upgrading from 0.3
- Python 3.11+ is required; the
ideasandtoken-utilsdependencies are gone. - Arguments reach the command exactly as a shell would pass them. Previously backslashes were
re-interpreted by Python (
'a\\nb'became a newline) and a"inside an argument produced invalid code. {{ x }}is converted withstr(), may contain spaces and quotes, and no longer raisesInvalidInterpolationfor them.- Interpolated values in
>commands are now quoted. Use{{! x }}for the old raw behavior. >commands keep their quoting; previously quotes were stripped before the shell saw them.a = $x | ynow captures the pipeline's output instead of assigning a process object, and nocmd1/foutvariables leak into your code.$a > f1 > f2writes the output to both files; previously the second file received a copy of the first.&&,||,;,2>,&>,2>&1,|&and$( )are new.&&used to raiseNotImplementedError.- Generated lists are formatted
["ls", "-la"]and the result no longer ends with an added newline. - Text such as
"total = $5"or"($x)"inside a Python string is no longer mistaken for a command.
Development
uv sync
make check # format, lint (ruff + mypy), test
make demo
Metadata
Release files for PyBash 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pybash-0.4.0.tar.gz | 47.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pybash-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.3 kB
Release files / pybash-0.4.0.tar.gz
| Download URL | pybash-0.4.0.tar.gz |
|---|---|
| Size | 47.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5c73e5ad1d07057dab29f8281108f9aada0a47708b71da0b8a37a9fa1398ccc3
|
|
BLAKE2b-256 checksum How to use checksums |
53dfe876fe2f96a1389d87596e0e1613a9dcadc9f542477818335a2c19f32b99
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pybash-0.4.0-py3-none-any.whl
| Download URL | pybash-0.4.0-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
03a4703d0562c0fc4683e3ef3dac87af52059990c7ab0ba17b45fdbd5cc120fa
|
|
BLAKE2b-256 checksum How to use checksums |
795575b84ea693567e4782b7f522368d90fd5261d9d2e3df84c24fba5b323ca8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|