Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
 
 
 
 

readme.org

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 result
260K	/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.retcode
6
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}").retcode
0
z("test -n {False:bool}").retcode
1

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_str
 echo 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}") # unsafe
do 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 -c can 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.

About

Use persistent (or not) zsh sessions from Python, with near first-party interoperability between the two.

Topics

Resources

Releases

No releases published

Packages

No packages published
You can’t perform that action at this time.