Skip to content

Latest commit

 

History

History
924 lines (790 loc) · 30.3 KB

readme.org

File metadata and controls

924 lines (790 loc) · 30.3 KB

Brish

Alltime Downloads Monthly Downloads MIT License GPL3 License

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
name="A$ron"
z("echo Hello {name}")
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='2021\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
524K	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97
524K	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82
1.3M	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420
1.3M	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr
  0B	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/34cc02221710caf309bff5ca96808d7a
520K	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1
2.9M	/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43
  0B	/Users/evar/tmp/8826215ac0ed61d617906f658322fce7
348K	/Users/evar/tmp/IMG_0396.PNG
 44K	/Users/evar/tmp/a2.jpg
 40K	/Users/evar/tmp/a4.jpg
8.0K	/Users/evar/tmp/bills
  0B	/Users/evar/tmp/garden
468K	/Users/evar/tmp/image-14000213234237913.png
 40K	/Users/evar/tmp/photo_2021-05-08_00-35-24.jpg
152K	/Users/evar/tmp/photo_2021-05-08_00-55-29.jpg
8.0K	/Users/evar/tmp/tumblr
4.0M	/Users/evar/tmp/tumblr_2c0ad7a3fba563996c9abaedc5e8d4f7_356ef3d9_1280.gif
576K	/Users/evar/tmp/tumblr_5a2868650b058c42a7d141b8a2f474bc_eac04dc0_1280.jpg
976K	/Users/evar/tmp/tumblr_5cc2e0e48418ec3c9eb200d151daf647_e44e419b_1280.jpg
 44K	/Users/evar/tmp/tumblr_6c90d77a676cf20fc096cc19220af4ab_e124dbec_540.gif.mp4
  0B	/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg
4.0K	/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg.aria2
656K	/Users/evar/tmp/tumblr_bc2259b471f792065eb6707b7c29d27e_97f97d94_1280.jpg
1.0M	/Users/evar/tmp/tumblr_ec36fde70ee0264cdc2f61f394181c61_575227e7_1280.jpg
 84K	/Users/evar/tmp/view.php
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 (134093 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 robust.

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/83e93d36396014e0cd979ddcad2d9d43
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/34cc02221710caf309bff5ca96808d7a
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420
/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420/tumblr_dreamcorp420_650543836474589184_01.gif
/Users/evar/tmp/8826215ac0ed61d617906f658322fce7
/Users/evar/tmp/IMG_0396.PNG
/Users/evar/tmp/a2.jpg
/Users/evar/tmp/a4.jpg
/Users/evar/tmp/bills
/Users/evar/tmp/garden
/Users/evar/tmp/image-14000213234237913.png
/Users/evar/tmp/photo_2021-05-08_00-35-24.jpg
/Users/evar/tmp/photo_2021-05-08_00-55-29.jpg
/Users/evar/tmp/tumblr
/Users/evar/tmp/tumblr_2c0ad7a3fba563996c9abaedc5e8d4f7_356ef3d9_1280.gif
/Users/evar/tmp/tumblr_5a2868650b058c42a7d141b8a2f474bc_eac04dc0_1280.jpg
/Users/evar/tmp/tumblr_5cc2e0e48418ec3c9eb200d151daf647_e44e419b_1280.jpg
/Users/evar/tmp/tumblr_6c90d77a676cf20fc096cc19220af4ab_e124dbec_540.gif.mp4
/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg
/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg.aria2
/Users/evar/tmp/tumblr_bc2259b471f792065eb6707b7c29d27e_97f97d94_1280.jpg
/Users/evar/tmp/tumblr_ec36fde70ee0264cdc2f61f394181c61_575227e7_1280.jpg
/Users/evar/tmp/view.php

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:
’Tis said that the Passion Flower,
   With its figures of spear and sword
And hammer and nails, is a symbol
   Of the Woe of our Blessed Lord.
So still in the Heart of Beauty
   Has been hidden, since Life drew breath,
The sword and the spear of Anguish,
   And the hammer and nails of Death.
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.

Parallel Execution Using server_count

server_count allows the underlying zsh instance of a Brish object to fork that many times, and so serve that many clients in parallel. This will not increase the startup time, as the forking happens after loading the zsh interpreter completely.

I have combined this with GNU parallel to easily parallelize my zsh functions.

n = 32
my_parallel_brish = Brish(server_count=n)

import logging
import threading
import time

def thread_function(name):
    logging.info("Thread %s: starting", name)
    my_parallel_brish.zp("echo Started {name} at $EPOCHREALTIME ; sleep 10 ; echo Finished {name} at $EPOCHREALTIME")
    logging.info("Thread %s: finishing", name)

if __name__ == "__main__":
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO,
                        datefmt="%H:%M:%S")

    threads = list()
    now = float(z("echo $EPOCHREALTIME").outrs)
    for index in range(32):
        logging.info("Main    : create and start thread %d.", index)
        x = threading.Thread(target=thread_function, args=(index,))
        threads.append(x)
        x.start()

    for index, thread in enumerate(threads):
        logging.info("Main    : before joining thread %d.", index)
        thread.join()
        logging.info("Main    : thread %d done", index)

    end = float(z("echo $EPOCHREALTIME").outrs)
    print(f"Took {(end - now)}")


17:25:26: Main    : create and start thread 0.
17:25:26: Thread 0: starting
17:25:26: Main    : create and start thread 1.
17:25:26: Thread 1: starting
17:25:26: Main    : create and start thread 2.
17:25:26: Thread 2: starting
17:25:26: Main    : create and start thread 3.
17:25:26: Thread 3: starting
17:25:26: Main    : create and start thread 4.
17:25:26: Thread 4: starting
17:25:26: Main    : create and start thread 5.
17:25:26: Thread 5: starting
17:25:26: Main    : create and start thread 6.
17:25:26: Thread 6: starting
17:25:26: Main    : create and start thread 7.
17:25:26: Thread 7: starting
17:25:26: Main    : create and start thread 8.
17:25:26: Thread 8: starting
17:25:26: Main    : create and start thread 9.
17:25:26: Thread 9: starting
17:25:26: Main    : create and start thread 10.
17:25:26: Thread 10: starting
17:25:26: Main    : create and start thread 11.
17:25:26: Thread 11: starting
17:25:26: Main    : create and start thread 12.
17:25:26: Thread 12: starting
17:25:26: Main    : create and start thread 13.
17:25:26: Thread 13: starting
17:25:26: Main    : create and start thread 14.
17:25:26: Thread 14: starting
17:25:26: Main    : create and start thread 15.
17:25:26: Thread 15: starting
17:25:26: Main    : create and start thread 16.
17:25:26: Thread 16: starting
17:25:26: Main    : create and start thread 17.
17:25:26: Thread 17: starting
17:25:26: Main    : create and start thread 18.
17:25:26: Thread 18: starting
17:25:26: Main    : create and start thread 19.
17:25:26: Thread 19: starting
17:25:26: Main    : create and start thread 20.
17:25:26: Thread 20: starting
17:25:26: Main    : create and start thread 21.
17:25:26: Thread 21: starting
17:25:26: Main    : create and start thread 22.
17:25:26: Thread 22: starting
17:25:26: Main    : create and start thread 23.
17:25:26: Thread 23: starting
17:25:26: Main    : create and start thread 24.
17:25:26: Thread 24: starting
17:25:26: Main    : create and start thread 25.
17:25:26: Thread 25: starting
17:25:26: Main    : create and start thread 26.
17:25:26: Thread 26: starting
17:25:26: Main    : create and start thread 27.
17:25:26: Thread 27: starting
17:25:26: Main    : create and start thread 28.
17:25:26: Thread 28: starting
17:25:26: Main    : create and start thread 29.
17:25:26: Thread 29: starting
17:25:26: Main    : create and start thread 30.
17:25:26: Thread 30: starting
17:25:26: Main    : create and start thread 31.
17:25:26: Thread 31: starting
17:25:26: Main    : before joining thread 0.
Started 0 at 1620651326.2126729488
Finished 0 at 1620651336.2229239941
17:25:36: Thread 0: finishing
17:25:36: Main    : thread 0 done
17:25:36: Main    : before joining thread 1.
Started 1 at 1620651327.2022259235
Finished 1 at 1620651337.2120540142
17:25:37: Thread 1: finishing
17:25:37: Main    : thread 1 done
17:25:37: Main    : before joining thread 2.
Started 30 at 1620651327.2101778984
Finished 30 at 1620651337.2140960693
17:25:37: Thread 30: finishing
Started 2 at 1620651328.2068090439
Finished 2 at 1620651338.2182691097
17:25:38: Thread 2: finishing
17:25:38: Main    : thread 2 done
17:25:38: Main    : before joining thread 3.
Started 31 at 1620651328.2222359180
Finished 31 at 1620651338.2338199615
17:25:38: Thread 31: finishing
Started 7 at 1620651329.2063989639
Finished 7 at 1620651339.2115590572
17:25:39: Thread 7: finishing
Started 15 at 1620651330.2087130547
Finished 15 at 1620651340.2192440033
17:25:40: Thread 15: finishing
Started 21 at 1620651331.2160348892
Finished 21 at 1620651341.2246019840
17:25:41: Thread 21: finishing
Started 23 at 1620651332.2160398960
Finished 23 at 1620651342.2200219631
17:25:42: Thread 23: finishing
Started 9 at 1620651333.2236700058
Finished 9 at 1620651343.2359619141
17:25:43: Thread 9: finishing
Started 18 at 1620651334.2257950306
Finished 18 at 1620651344.2365601063
17:25:44: Thread 18: finishing
Started 12 at 1620651335.2241439819
Finished 12 at 1620651345.2335329056
17:25:45: Thread 12: finishing
Started 20 at 1620651336.2342200279
Finished 20 at 1620651346.2429049015
17:25:46: Thread 20: finishing
Started 16 at 1620651337.4859669209
Finished 16 at 1620651347.4899230003
17:25:47: Thread 16: finishing
Started 22 at 1620651338.2339038849
Finished 22 at 1620651348.2375440598
17:25:48: Thread 22: finishing
Started 19 at 1620651339.2459530830
Finished 19 at 1620651349.2504169941
17:25:49: Thread 19: finishing
Started 13 at 1620651340.2416980267
Finished 13 at 1620651350.2485001087
17:25:50: Thread 13: finishing
Started 10 at 1620651340.2490129471
Finished 10 at 1620651350.2568130493
17:25:50: Thread 10: finishing
Started 29 at 1620651341.2439520359
Finished 29 at 1620651351.2504179478
17:25:51: Thread 29: finishing
Started 25 at 1620651342.2465701103
Finished 25 at 1620651352.2498950958
17:25:52: Thread 25: finishing
Started 17 at 1620651343.2493131161
Finished 17 at 1620651353.2571830750
17:25:53: Thread 17: finishing
Started 28 at 1620651344.2550890446
Finished 28 at 1620651354.2586359978
17:25:54: Thread 28: finishing
Started 14 at 1620651345.2569661140
Finished 14 at 1620651355.2659308910
17:25:55: Thread 14: finishing
Started 5 at 1620651346.2559928894
Finished 5 at 1620651356.2631940842
17:25:56: Thread 5: finishing
Started 4 at 1620651347.2538421154
Finished 4 at 1620651357.2619009018
17:25:57: Thread 4: finishing
Started 3 at 1620651347.2638580799
Finished 3 at 1620651357.2686970234
17:25:57: Thread 3: finishing
17:25:57: Main    : thread 3 done
17:25:57: Main    : before joining thread 4.
17:25:57: Main    : thread 4 done
17:25:57: Main    : before joining thread 5.
17:25:57: Main    : thread 5 done
17:25:57: Main    : before joining thread 6.
Started 26 at 1620651348.2553079128
Finished 26 at 1620651358.2628009319
17:25:58: Thread 26: finishing
Started 27 at 1620651348.2706210613
Finished 27 at 1620651358.2781529427
17:25:58: Thread 27: finishing
Started 24 at 1620651349.2586579323
Finished 24 at 1620651359.2646100521
17:25:59: Thread 24: finishing
Started 11 at 1620651350.2648739815
Finished 11 at 1620651360.2702779770
17:26:00: Thread 11: finishing
Started 8 at 1620651351.2621378899
Finished 8 at 1620651361.2658278942
17:26:01: Thread 8: finishing
Started 6 at 1620651352.4786870480
Finished 6 at 1620651362.4896230698
17:26:02: Thread 6: finishing
17:26:02: Main    : thread 6 done
17:26:02: Main    : before joining thread 7.
17:26:02: Main    : thread 7 done
17:26:02: Main    : before joining thread 8.
17:26:02: Main    : thread 8 done
17:26:02: Main    : before joining thread 9.
17:26:02: Main    : thread 9 done
17:26:02: Main    : before joining thread 10.
17:26:02: Main    : thread 10 done
17:26:02: Main    : before joining thread 11.
17:26:02: Main    : thread 11 done
17:26:02: Main    : before joining thread 12.
17:26:02: Main    : thread 12 done
17:26:02: Main    : before joining thread 13.
17:26:02: Main    : thread 13 done
17:26:02: Main    : before joining thread 14.
17:26:02: Main    : thread 14 done
17:26:02: Main    : before joining thread 15.
17:26:02: Main    : thread 15 done
17:26:02: Main    : before joining thread 16.
17:26:02: Main    : thread 16 done
17:26:02: Main    : before joining thread 17.
17:26:02: Main    : thread 17 done
17:26:02: Main    : before joining thread 18.
17:26:02: Main    : thread 18 done
17:26:02: Main    : before joining thread 19.
17:26:02: Main    : thread 19 done
17:26:02: Main    : before joining thread 20.
17:26:02: Main    : thread 20 done
17:26:02: Main    : before joining thread 21.
17:26:02: Main    : thread 21 done
17:26:02: Main    : before joining thread 22.
17:26:02: Main    : thread 22 done
17:26:02: Main    : before joining thread 23.
17:26:02: Main    : thread 23 done
17:26:02: Main    : before joining thread 24.
17:26:02: Main    : thread 24 done
17:26:02: Main    : before joining thread 25.
17:26:02: Main    : thread 25 done
17:26:02: Main    : before joining thread 26.
17:26:02: Main    : thread 26 done
17:26:02: Main    : before joining thread 27.
17:26:02: Main    : thread 27 done
17:26:02: Main    : before joining thread 28.
17:26:02: Main    : thread 28 done
17:26:02: Main    : before joining thread 29.
17:26:02: Main    : thread 29 done
17:26:02: Main    : before joining thread 30.
17:26:02: Main    : thread 30 done
17:26:02: Main    : before joining thread 31.
17:26:02: Main    : thread 31 done
Took 36.33210492134094

Running in the Background

The z_background function allows you to execute shell commands asynchronously in a new thread. It starts a new Zsh instance and runs the given command without blocking your main Python thread. This is particularly useful when you want to perform non-blocking operations or execute long-running shell commands without interrupting your Python program’s flow.

from brish import z_background

msg = "You can’t make an omelet without breaking a few eggs."
result_future = z_background(
    "say {msg}",
    # Needs the `say` command, available by default on macOS
)
result_future
<Future at 0x1435b51e0 state=running>

The z_background function returns a Future object.

result_future.result()
CmdResult(retcode=0, out='', err='', cmd=" say 'You can’t make an omelet without breaking a few eggs.' ", cmd_stdin='')

Here is another example:

from brish import z_background
import concurrent.futures

# Define multiple commands
commands = [
    "sleep 5 && echo 'First command completed.'",
    "sleep 3 && echo 'Second command completed.'",
    "sleep 1 && echo 'Third command completed.'",
]

# Execute all commands asynchronously
futures = [z_background(cmd) for cmd in commands]

for future in concurrent.futures.as_completed(futures):
    result = future.result()
    print(result.out)
Third command completed.

Second command completed.

First command completed.

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., running eval on user input:

untrusted_input = " ; echo do evil | cat"
z("eval {untrusted_input}") # unsafe
do evil
z("echo {untrusted_input}") # safe
; echo do evil | cat

Known Issues

  • Piping binary (non-text) output from zsh to Python does not work
  • Nonstandard encodings (non UTF-8) are corrupted
    z("echo 'sth × another (ver.-)'")
        
    sth Ã\xb7 another (ver.-)
        
  • There is always sth piped to the standard input (an empty string by default). This can alter the behavior of some commands such as ripgrep; Using </dev/null or <&- can be a suitable workaround.

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 usecase; 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.