Brish
Guide
Installation
pip install -U brish
Or install the latest master (recommended, as I might have forgotten to push a new versioned update):
pip install git+https://github.com/NightMachinary/brish
You need a recent Python version, as Brish uses some of the newer metaprogramming APIs. Obviously, you also need zsh installed.
Quickstart
from brish import z, zp, Brish<64;35;16M
name="A$ron"
z("echo Hello {name}")<64;16;16M4;16;16M: Hello A$ron
z automatically converts Python lists to shell lists:
alist = ["# Fruits", "1. Orange", "2. Rambutan", "3. Strawberry"]
z("for i in {alist} ; do echo $i ; done")# Fruits 1. Orange 2. Rambutan 3. Strawberry
z returns a CmdResult (more about which later):
res = z("date +%Y")
repr(res)CmdResult(retcode=0, out='2020\n', err='', cmd=' date +%Y ', cmd_stdin='')
You can use zp as a shorthand for print(z(...).outerr, end=''):
for i in range(10):
cmd = "(( {i} % 2 == 0 )) && echo {i} || {{ echo Bad Odds'!' >&2 }}" # Using {{ and }} as escapes for { and }
zp(cmd)
print(f"Same thing: {z(cmd).outerr}", end='')0 Same thing: 0 Bad Odds! Same thing: Bad Odds! 2 Same thing: 2 Bad Odds! Same thing: Bad Odds! 4 Same thing: 4 Bad Odds! Same thing: Bad Odds! 6 Same thing: 6 Bad Odds! Same thing: Bad Odds! 8 Same thing: 8 Bad Odds! Same thing: Bad Odds!
CmdResult is true if its return code is zero:
if z("test -e ~/"):
print("HOME exists!")
else:
print("We're homeless :(")HOME exists!
CmdResult is smart about iterating:
for path in z("command ls ~/tmp/"): # `command` helps bypass potential aliases defined on `ls`
zp("du -h ~/tmp/{path}") # zp prints the result260K /Users/evar/tmp/a.png 4.8M /Users/evar/tmp/bug.mkv 0B /Users/evar/tmp/garden
res = z("""echo This is stdout
echo This is stderr >&2
(exit 6) # this is the return code""")
repr(res.out)This is stdout\n
CmdResult.outrs strips the final newlines:
repr(res.outrs)This is stdout
repr(res.err)This is stderr\n
res.retcode6
res.longstr
cmd: echo This is stdout
echo This is stderr >&2
(exit 6) # this is the return code
stdout:
This is stdout
stderr:
This is stderr
return code: 6
By default, z doesn’t fork. So we can use it to change the state of the running zsh session:
z("""
(($+commands[imdbpy])) || pip install -U imdbpy
imdb() imdbpy search movie --first "$*"
""")
z("imdb Into the Woods 2014")Movie ===== Title: Into the Woods (2014) Genres: Adventure, Comedy, Drama, Fantasy, Musical. Director: Rob Marshall. Writer: James Lapine, James Lapine. Cast: Anna Kendrick (Cinderella), Daniel Huttlestone (Jack), James Corden (Baker / Narrator), Emily Blunt (Baker's Wife), Christine Baranski (Stepmother). Runtime: 125. Country: United States. Language: English. Rating: 5.9 (129612 votes). Plot: A witch tasks a childless baker and his wife with procuring magical items from classic fairy tales to reverse the curse put on their family tree.
We can force a fork. This is useful to make your scripts more robusts.
print(z("exit 7", fork=True).retcode)
zp("echo 'Still alive!'")7 Still alive!
Working with stdin:
# the intuitive way
a="""1
2
3
4
5
"""
z("<<<{a} wc -l")6
z("wc -l", cmd_stdin=a)5
More details
The stdin will by default be set to the empty string:
zp("cat")
zp("echo 'as you see, the previous command produced no output. It also did not block.'")as you see, the previous command produced no output. It also did not block.
z escapes your Python variables automagically:
python_var = "$HOME"
z("echo {python_var}")$HOME
Turning off the auto-escape:
z("echo {python_var:e}")/Users/evar
Working with Python bools from the shell:
z("test -n {True:bool}").retcode0
z("test -n {False:bool}").retcode1
Working with NUL-terminated output:
for f in z("fd -0 . ~/tmp").iter0():
zp("echo {f}")/Users/evar/tmp/a.png /Users/evar/tmp/bug.mkv /Users/evar/tmp/garden
You can bypass the automatic iterable conversion by converting the iterable to a string first:
z("echo {' '.join(map(str,alist))}")# Fruits 1. Orange 2. Rambutan 3. Strawberry
Normal Python formatting syntax works as expected:
z("echo {67:f}")67.0
z("echo {[11, 45]!s}")[11, 45]
You can obviously nest your z calls:
z("""echo monkey$'\n'{z("curl -s https://www.poemist.com/api/v1/randompoems | jq --raw-output '.[0].content'")}$'\n'end | sed -e 's/monkey/Random Poem:/'""")Random Poem: 114 Good night, because we must, How intricate the dust! I would go, to know! Oh incognito! Saucy, Saucy Seraph To elude me so! Father! they won't tell me, Won't you tell them to? end
The Brish Class
z and zp are just convenience methods:
bsh = Brish() z = bsh.z zp = bsh.zp zq = bsh.zsh_quote zs = bsh.zstring
You can use Brish instances yourself (all arguments to it are optional). The boot command boot_cmd allows you to easily initialize the zsh session:
my_own_brish = Brish(boot_cmd="mkdir -p ~/tmp ; cd ~/tmp")
my_own_brish.z("echo $PWD")/Users/evar/tmp
Brish.z itself is sugar around Brish.zstring and Brish.send_cmd:
cmd_str = my_own_brish.zstring("echo zstring constructs the command string that will be sent to zsh. It interpolates the Pythonic variables: {python_var} {alist}")
cmd_strecho zstring constructs the command string that will be sent to zsh. It interpolates the Pythonic variables: '$HOME' '# Fruits' '1. Orange' '2. Rambutan' '3. Strawberry'
my_own_brish.send_cmd(cmd_str)zstring constructs the command string that will be sent to zsh. It interpolates the Pythonic variables: $HOME # Fruits 1. Orange 2. Rambutan 3. Strawberry
You can restart a Brish instance:
my_own_brish.z("a=56")
my_own_brish.zp("echo Before restart: $a")
my_own_brish.restart()
my_own_brish.zp("echo After restart: $a")
my_own_brish.zp("echo But the boot_cmd has run in the restarted instance, too: $PWD")Before restart: 56 After restart: But the boot_cmd has run in the restarted instance, too: /Users/evar/tmp
Brish is threadsafe. I have built BrishGarden on top of Brish to provide an HTTP REST API for executing zsh code (if wanted, in sessions). Using BrishGarden, you can embed zsh in pretty much any programming language, and pay no cost whatsoever for its startup. It can also function as a remote code executor.
Security considerations
I am not a security expert, and security doesn’t come by default in these situations. So be careful if you use untrusted input in the commands fed to zsh. Nevertheless, I can’t imagine any (non-obvious) attack vectors, as the input gets automatically escaped by default. Feedback by security experts will be appreciated.
Note that you can create security holes for yourself, by, e.g., `eval`ing user input:
untrusted_input = " ; echo do evil | cat"
z("eval {untrusted_input}") # unsafedo evil
z("echo {untrusted_input}") # safe; echo do evil | cat
Future features
I like to add a mode where the zsh session inherits the stderr from the parent Python process. This allows usage of interactive programs like fzf.
If you have any good design ideas, create an issue!
Related projects
- pysh uses comments in bash scripts to switch the interpreter to Python, allowing variable reuse between the two.
- plumbum is a small yet feature-rich library for shell script-like programs in Python. It attempts to mimic the shell syntax (“shell combinators”) where it makes sense, while keeping it all Pythonic and cross-platform. I personally like this one a lot. A robust option that is also easy-to-use.
- shellfuncs: Python API to execute shell functions as they would be Python functions. (Last commit is in 2017.)
- xonsh is a superset of Python 3.5+ with additional shell primitives.
- daudin tries to eval your code as Python, falling back to the shell if that fails. It does not currently reuse a shell session, thus incurring large overhead. I think it can use Brish to solve this, but someone needs to contribute the support.
- duct.py is a library for running child processes. It’s quite low-level compared to the other projects in this list.
python -ccan also be powerful, especially if you write yourself a helper library in Python and some wrappers in your shell dotfiles. An example:alias x='noglob calc-raw' calc-raw () { python3 -c "from math import *; print($*)" }- Z shell kernel for Jupyter Notebook allows you to do all sorts of stuff if you spend the time implementing your <65;45;18Musecase; See emacs-jupyter to get a taste of what’s possible. Jupyter Kernel Gateway also sounds promising, but I haven’t tried it out yet. Beware the completion support in this kernel though. It uses a pre-alpha proof of concept thingy that was very buggy when I tried it.
- Finally, if you’re feeling adventurous, try Rust’s rust_cmd_lib. It’s quite beautiful.
Licenses
Dual-licensed under MIT and GPL v3 or later.