{"id":23106955,"url":"https://github.com/NightMachinery/brish","last_synced_at":"2025-08-16T17:31:15.426Z","repository":{"id":44617726,"uuid":"255323234","full_name":"NightMachinery/brish","owner":"NightMachinery","description":"Safely embed Zsh in Python.","archived":false,"fork":false,"pushed_at":"2024-10-28T15:16:49.000Z","size":111,"stargazers_count":14,"open_issues_count":8,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-12-15T03:43:05.719Z","etag":null,"topics":["bridge","exec","execute","executor","interop","interoperability","interpreters","modern-shell","process-manager","python","shell","subprocess","xonsh","zsh"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NightMachinery.png","metadata":{"files":{"readme":"readme.org","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-13T12:41:52.000Z","updated_at":"2024-12-12T23:51:10.000Z","dependencies_parsed_at":"2024-03-17T17:29:28.849Z","dependency_job_id":"ac53bd8d-d828-4249-bd7b-ff4c745371d6","html_url":"https://github.com/NightMachinery/brish","commit_stats":{"total_commits":67,"total_committers":4,"mean_commits":16.75,"dds":"0.13432835820895528","last_synced_commit":"c784c28722b25f72b3a3e0d73ff23002503b1a4c"},"previous_names":["nightmachinary/brish"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NightMachinery%2Fbrish","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NightMachinery%2Fbrish/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NightMachinery%2Fbrish/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NightMachinery%2Fbrish/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NightMachinery","download_url":"https://codeload.github.com/NightMachinery/brish/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230047254,"owners_count":18164573,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bridge","exec","execute","executor","interop","interoperability","interpreters","modern-shell","process-manager","python","shell","subprocess","xonsh","zsh"],"created_at":"2024-12-17T01:11:56.517Z","updated_at":"2024-12-17T01:11:57.735Z","avatar_url":"https://github.com/NightMachinery.png","language":"Python","readme":"#+TITLE: Brish\n\n#+begin_html\n\u003ca href=\"https://pepy.tech/project/brish\"\u003e\n\u003cimg alt=\"Alltime Downloads\" src=\"https://pepy.tech/badge/brish\" /\u003e\n\u003c/a\u003e\n\n\u003ca href=\"https://pepy.tech/project/brish\"\u003e\n\u003cimg alt=\"Monthly Downloads\" src=\"https://pepy.tech/badge/brish/month\" /\u003e\n\u003c/a\u003e\n\n\u003ca href=\"https://opensource.org/licenses/MIT\"\u003e\n\u003cimg alt=\"MIT License\" src=\"https://img.shields.io/badge/license-MIT-blue.svg\" /\u003e\n\u003c/a\u003e\n\n\u003ca href=\"http://www.gnu.org/licenses/gpl-3.0.html\"\u003e\n\u003cimg alt=\"GPL3 License\" src=\"http://img.shields.io/:license-gpl3-blue.svg\" /\u003e\n\u003c/a\u003e\n#+end_html\n\n* Guide\n** Installation\n\n~pip install -U brish~\n\nOr install the latest master (recommended, as I might have forgotten to push a new versioned update):\n\n~pip install git+https://github.com/NightMachinary/brish~\n\nYou need a recent Python version, as Brish uses some of the newer metaprogramming APIs. Obviously, you also need zsh installed.\n\n** Quickstart\n\n#+begin_src python :session p1 :results silent :tangle tests/test_tangled1.py\nfrom brish import z, zp, Brish\n#+end_src\n\n#+begin_src python :session p1 :results silent :exports none :tangle tests/test_tangled1.py\nNI = True\n#+end_src\n\n#+begin_src python :session p1 :results silent :exports none\nNI = False\n#+end_src\n\n#+name: t1\n#+begin_src python :session p1 :results value :exports both :tangle tests/test_tangled1.py\nname=\"A$ron\"\nz(\"echo Hello {name}\")\n#+end_src\n\n#+RESULTS: t1\n#+begin_example\nHello A$ron\n#+end_example\n\n\n#+begin_src python :session p1 :var t1=t1 :results value :exports none :tangle tests/test_tangled1.py\ndef test1():\n    assert t1 == \"Hello A$ron\"\n    return True\nNI or test1()\n#+end_src\n\n#+RESULTS:\n#+begin_example\nTrue\n#+end_example\n\n~z~ automatically converts Python lists to shell lists:\n#+name: t2\n#+begin_src python :session p1 :results value :exports both :tangle tests/test_tangled1.py\nalist = [\"# Fruits\", \"1. Orange\", \"2. Rambutan\", \"3. Strawberry\"]\nz(\"for i in {alist} ; do echo $i ; done\")\n#+end_src\n\n#+RESULTS: t2\n#+begin_example\n# Fruits\n1. Orange\n2. Rambutan\n3. Strawberry\n#+end_example\n\n#+begin_src python :session p1 :var t2=t2 :results value :exports none :tangle tests/test_tangled1.py\ndef test2():\n    assert t2 == \"\"\"# Fruits\n1. Orange\n2. Rambutan\n3. Strawberry\"\"\"\nNI or test2()\n#+end_src\n\n#+RESULTS:\n#+begin_example\nNone\n#+end_example\n\n~z~ returns a ~CmdResult~ (more about which later):\n\n#+begin_src python :session p1 :results value :exports both\nres = z(\"date +%Y\")\nrepr(res)\n#+end_src\n\n#+RESULTS:\n#+begin_example\nCmdResult(retcode=0, out='2021\\n', err='', cmd=' date +%Y ', cmd_stdin='')\n#+end_example\n\nYou can use ~zp~ as a shorthand for ~print(z(...).outerr, end='')~:\n\n#+begin_src python :session p1 :results output :exports both\nfor i in range(10):\n    cmd = \"(( {i} % 2 == 0 )) \u0026\u0026 echo {i} || {{ echo Bad Odds'!' \u003e\u00262 }}\" # Using {{ and }} as escapes for { and }\n    zp(cmd)\n    print(f\"Same thing: {z(cmd).outerr}\", end='')\n#+end_src\n\n#+RESULTS:\n#+begin_example\n0\nSame thing: 0\nBad Odds!\nSame thing: Bad Odds!\n2\nSame thing: 2\nBad Odds!\nSame thing: Bad Odds!\n4\nSame thing: 4\nBad Odds!\nSame thing: Bad Odds!\n6\nSame thing: 6\nBad Odds!\nSame thing: Bad Odds!\n8\nSame thing: 8\nBad Odds!\nSame thing: Bad Odds!\n#+end_example\n\n~CmdResult~ is true if its return code is zero:\n#+name: t3\n#+begin_src python :session p1 :results output :exports both :tangle tests/test_tangled1.py\nif z(\"test -e ~/\"):\n    print(\"HOME exists!\")\nelse:\n    print(\"We're homeless :(\")\n#+end_src\n\n#+RESULTS: t3\n#+begin_example\nHOME exists!\n#+end_example\n\n#+begin_src python :session p1 :var t3=t3 :results value :exports none :tangle tests/test_tangled1.py\nassert t3 == \"HOME exists!\"\n#+end_src\n\n#+RESULTS:\n\n~CmdResult~ is smart about iterating:\n#+name: t4\n#+begin_src python :session p1 :results output :exports both :tangle tests/test_tangled1.py\nfor path in z(\"command ls ~/tmp/\"): # `command` helps bypass potential aliases defined on `ls`\n    zp(\"du -h ~/tmp/{path}\") # zp prints the result\n#+end_src\n\n#+RESULTS: t4\n#+begin_example\n524K\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97\n524K\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82\n1.3M\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420\n1.3M\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr\n  0B\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/34cc02221710caf309bff5ca96808d7a\n520K\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1\n2.9M\t/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43\n  0B\t/Users/evar/tmp/8826215ac0ed61d617906f658322fce7\n348K\t/Users/evar/tmp/IMG_0396.PNG\n 44K\t/Users/evar/tmp/a2.jpg\n 40K\t/Users/evar/tmp/a4.jpg\n8.0K\t/Users/evar/tmp/bills\n  0B\t/Users/evar/tmp/garden\n468K\t/Users/evar/tmp/image-14000213234237913.png\n 40K\t/Users/evar/tmp/photo_2021-05-08_00-35-24.jpg\n152K\t/Users/evar/tmp/photo_2021-05-08_00-55-29.jpg\n8.0K\t/Users/evar/tmp/tumblr\n4.0M\t/Users/evar/tmp/tumblr_2c0ad7a3fba563996c9abaedc5e8d4f7_356ef3d9_1280.gif\n576K\t/Users/evar/tmp/tumblr_5a2868650b058c42a7d141b8a2f474bc_eac04dc0_1280.jpg\n976K\t/Users/evar/tmp/tumblr_5cc2e0e48418ec3c9eb200d151daf647_e44e419b_1280.jpg\n 44K\t/Users/evar/tmp/tumblr_6c90d77a676cf20fc096cc19220af4ab_e124dbec_540.gif.mp4\n  0B\t/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg\n4.0K\t/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg.aria2\n656K\t/Users/evar/tmp/tumblr_bc2259b471f792065eb6707b7c29d27e_97f97d94_1280.jpg\n1.0M\t/Users/evar/tmp/tumblr_ec36fde70ee0264cdc2f61f394181c61_575227e7_1280.jpg\n 84K\t/Users/evar/tmp/view.php\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nres = z(\"\"\"echo This is stdout\n           echo This is stderr \u003e\u00262\n           (exit 6) # this is the return code\"\"\")\nrepr(res.out)\n#+end_src\n\n#+RESULTS:\n#+begin_example\nThis is stdout\\n\n#+end_example\n\n~CmdResult.outrs~ strips the final newlines:\n\n#+begin_src python :session p1 :results value :exports both\nrepr(res.outrs)\n#+end_src\n\n#+RESULTS:\n#+begin_example\nThis is stdout\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nrepr(res.err)\n#+end_src\n\n#+RESULTS:\n#+begin_example\nThis is stderr\\n\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nres.retcode\n#+end_src\n\n#+RESULTS:\n#+begin_example\n6\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nres.longstr\n#+end_src\n\n#+RESULTS:\n#+begin_example\n\ncmd:  echo This is stdout\n           echo This is stderr \u003e\u00262\n           (exit 6) # this is the return code\nstdout:\nThis is stdout\n\nstderr:\nThis is stderr\n\nreturn code: 6\n#+end_example\n\nBy default, ~z~ doesn't fork. So we can use it to change the state of the running zsh session:\n#+begin_src python :session p1 :results value :exports both\nz(\"\"\"\n(($+commands[imdbpy])) || pip install -U imdbpy\nimdb() imdbpy search movie --first \"$*\"\n\"\"\")\nz(\"imdb Into the Woods 2014\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\nMovie\n=====\nTitle: Into the Woods (2014)\nGenres: Adventure, Comedy, Drama, Fantasy, Musical.\nDirector: Rob Marshall.\nWriter: James Lapine, James Lapine.\nCast: Anna Kendrick (Cinderella), Daniel Huttlestone (Jack), James Corden (Baker / Narrator), Emily Blunt (Baker's Wife), Christine Baranski (Stepmother).\nRuntime: 125.\nCountry: United States.\nLanguage: English.\nRating: 5.9 (134093 votes).\nPlot: 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.\n#+end_example\n\nWe can force a fork. This is useful to make your scripts more robust.\n#+begin_src python :session p1 :results output :exports both\nprint(z(\"exit 7\", fork=True).retcode)\nzp(\"echo 'Still alive!'\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n7\nStill alive!\n#+end_example\n\nWorking with stdin:\n#+begin_src python :session p1 :results value :exports both\n# the intuitive way\na=\"\"\"1\n2\n3\n4\n5\n\"\"\"\nz(\"\u003c\u003c\u003c{a} wc -l\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n6\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nz(\"wc -l\", cmd_stdin=a)\n#+end_src\n\n#+RESULTS:\n#+begin_example\n5\n#+end_example\n\n** More Details\nThe stdin will by default be set to the empty string:\n#+begin_src python :session p1 :results output :exports both\nzp(\"cat\")\nzp(\"echo 'As you see, the previous command produced no output. It also did not block.'\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\nas you see, the previous command produced no output. It also did not block.\n#+end_example\n\n~z~ escapes your Python variables automagically:\n#+begin_src python :session p1 :results value :exports both\npython_var = \"$HOME\"\nz(\"echo {python_var}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n$HOME\n#+end_example\n\nTurning off the auto-escape:\n#+begin_src python :session p1 :results value :exports both\nz(\"echo {python_var:e}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n/Users/evar\n#+end_example\n\nWorking with Python bools from the shell:\n#+begin_src python :session p1 :results value :exports both\nz(\"test -n {True:bool}\").retcode\n#+end_src\n\n#+RESULTS:\n#+begin_example\n0\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nz(\"test -n {False:bool}\").retcode\n#+end_src\n\n#+RESULTS:\n#+begin_example\n1\n#+end_example\n\nWorking with NUL-terminated output:\n#+begin_src python :session p1 :results output :exports both\nfor f in z(\"fd -0 . ~/tmp\").iter0():\n    zp(\"echo {f}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/34cc02221710caf309bff5ca96808d7a\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/36b77e6b3b7fde31f2fc4f182c0ecf82/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/6cc3d153426e2b6d1ac0f3736aaf74a1/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97/tumblr_9527f4f6d2f1a39ef2b839780831f38f_859e5e2b_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/c01ed1a32d65c8d4ecb9095509e61f97/tumblr_dd64a6ced93d19ffe78b47cf3439373d_e8e18fb0_2048.jpg\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420\n/Users/evar/tmp/83e93d36396014e0cd979ddcad2d9d43/tumblr/dreamcorp420/tumblr_dreamcorp420_650543836474589184_01.gif\n/Users/evar/tmp/8826215ac0ed61d617906f658322fce7\n/Users/evar/tmp/IMG_0396.PNG\n/Users/evar/tmp/a2.jpg\n/Users/evar/tmp/a4.jpg\n/Users/evar/tmp/bills\n/Users/evar/tmp/garden\n/Users/evar/tmp/image-14000213234237913.png\n/Users/evar/tmp/photo_2021-05-08_00-35-24.jpg\n/Users/evar/tmp/photo_2021-05-08_00-55-29.jpg\n/Users/evar/tmp/tumblr\n/Users/evar/tmp/tumblr_2c0ad7a3fba563996c9abaedc5e8d4f7_356ef3d9_1280.gif\n/Users/evar/tmp/tumblr_5a2868650b058c42a7d141b8a2f474bc_eac04dc0_1280.jpg\n/Users/evar/tmp/tumblr_5cc2e0e48418ec3c9eb200d151daf647_e44e419b_1280.jpg\n/Users/evar/tmp/tumblr_6c90d77a676cf20fc096cc19220af4ab_e124dbec_540.gif.mp4\n/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg\n/Users/evar/tmp/tumblr_70675efa5303a58292957ac942663309_f48499c2_1280.jpg.aria2\n/Users/evar/tmp/tumblr_bc2259b471f792065eb6707b7c29d27e_97f97d94_1280.jpg\n/Users/evar/tmp/tumblr_ec36fde70ee0264cdc2f61f394181c61_575227e7_1280.jpg\n/Users/evar/tmp/view.php\n#+end_example\n\nYou can bypass the automatic iterable conversion by converting the iterable to a string first:\n#+begin_src python :session p1 :results value :exports both\nz(\"echo {'    '.join(map(str,alist))}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n# Fruits    1. Orange    2. Rambutan    3. Strawberry\n#+end_example\n\nNormal Python formatting syntax works as expected:\n\n#+begin_src python :session p1 :results value :exports both\nz(\"echo {67:f}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n67.0\n#+end_example\n\n#+begin_src python :session p1 :exports both :results verbatim\nz(\"echo {[11, 45]!s}\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n[11, 45]\n#+end_example\n\n\nYou can obviously nest your ~z~ calls:\n#+begin_src python :session p1 :results value :exports both\nz(\"\"\"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:/'\"\"\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\nRandom Poem:\n’Tis said that the Passion Flower,\n   With its figures of spear and sword\nAnd hammer and nails, is a symbol\n   Of the Woe of our Blessed Lord.\nSo still in the Heart of Beauty\n   Has been hidden, since Life drew breath,\nThe sword and the spear of Anguish,\n   And the hammer and nails of Death.\nend\n#+end_example\n\n*** The Brish Class\n~z~ and ~zp~ are just convenience methods:\n\n#+begin_example\nbsh = Brish()\nz = bsh.z\nzp = bsh.zp\nzq = bsh.zsh_quote\nzs = bsh.zstring\n#+end_example\n\nYou can use ~Brish~ instances yourself (all arguments to it are optional). The boot command ~boot_cmd~ allows you to easily initialize the zsh session:\n\n#+begin_src python :session p1 :results value :exports both\nmy_own_brish = Brish(boot_cmd=\"mkdir -p ~/tmp ; cd ~/tmp\")\nmy_own_brish.z(\"echo $PWD\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\n/Users/evar/tmp\n#+end_example\n\n~Brish.z~ itself is sugar around ~Brish.zstring~ and ~Brish.send_cmd~:\n#+begin_src python :session p1 :results value :exports both\ncmd_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}\")\ncmd_str\n#+end_src\n\n#+RESULTS:\n#+begin_example\n 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'\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nmy_own_brish.send_cmd(cmd_str)\n#+end_src\n\n#+RESULTS:\n#+begin_example\nzstring constructs the command string that will be sent to zsh. It interpolates the Pythonic variables: $HOME # Fruits 1. Orange 2. Rambutan 3. Strawberry\n#+end_example\n\nYou can restart a Brish instance:\n#+begin_src python :session p1 :results output :exports both\nmy_own_brish.z(\"a=56\")\nmy_own_brish.zp(\"echo Before restart: $a\")\nmy_own_brish.restart()\nmy_own_brish.zp(\"echo After restart: $a\")\nmy_own_brish.zp(\"echo But the boot_cmd has run in the restarted instance, too: $PWD\")\n#+end_src\n\n#+RESULTS:\n#+begin_example\nBefore restart: 56\nAfter restart:\nBut the boot_cmd has run in the restarted instance, too: /Users/evar/tmp\n#+end_example\n\n~Brish~ is threadsafe. I have built [[https://github.com/NightMachinary/BrishGarden][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.\n\n**** Parallel Execution Using =server_count=\n=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.\n\nI have combined this with =GNU parallel= to easily parallelize my =zsh= functions.\n\n#+begin_src python :session p1 :results output :exports both\nn = 32\nmy_parallel_brish = Brish(server_count=n)\n\nimport logging\nimport threading\nimport time\n\ndef thread_function(name):\n    logging.info(\"Thread %s: starting\", name)\n    my_parallel_brish.zp(\"echo Started {name} at $EPOCHREALTIME ; sleep 10 ; echo Finished {name} at $EPOCHREALTIME\")\n    logging.info(\"Thread %s: finishing\", name)\n\nif __name__ == \"__main__\":\n    format = \"%(asctime)s: %(message)s\"\n    logging.basicConfig(format=format, level=logging.INFO,\n                        datefmt=\"%H:%M:%S\")\n\n    threads = list()\n    now = float(z(\"echo $EPOCHREALTIME\").outrs)\n    for index in range(32):\n        logging.info(\"Main    : create and start thread %d.\", index)\n        x = threading.Thread(target=thread_function, args=(index,))\n        threads.append(x)\n        x.start()\n\n    for index, thread in enumerate(threads):\n        logging.info(\"Main    : before joining thread %d.\", index)\n        thread.join()\n        logging.info(\"Main    : thread %d done\", index)\n\n    end = float(z(\"echo $EPOCHREALTIME\").outrs)\n    print(f\"Took {(end - now)}\")\n\n\n\n\n#+end_src\n\n#+RESULTS:\n#+begin_example\n17:25:26: Main    : create and start thread 0.\n17:25:26: Thread 0: starting\n17:25:26: Main    : create and start thread 1.\n17:25:26: Thread 1: starting\n17:25:26: Main    : create and start thread 2.\n17:25:26: Thread 2: starting\n17:25:26: Main    : create and start thread 3.\n17:25:26: Thread 3: starting\n17:25:26: Main    : create and start thread 4.\n17:25:26: Thread 4: starting\n17:25:26: Main    : create and start thread 5.\n17:25:26: Thread 5: starting\n17:25:26: Main    : create and start thread 6.\n17:25:26: Thread 6: starting\n17:25:26: Main    : create and start thread 7.\n17:25:26: Thread 7: starting\n17:25:26: Main    : create and start thread 8.\n17:25:26: Thread 8: starting\n17:25:26: Main    : create and start thread 9.\n17:25:26: Thread 9: starting\n17:25:26: Main    : create and start thread 10.\n17:25:26: Thread 10: starting\n17:25:26: Main    : create and start thread 11.\n17:25:26: Thread 11: starting\n17:25:26: Main    : create and start thread 12.\n17:25:26: Thread 12: starting\n17:25:26: Main    : create and start thread 13.\n17:25:26: Thread 13: starting\n17:25:26: Main    : create and start thread 14.\n17:25:26: Thread 14: starting\n17:25:26: Main    : create and start thread 15.\n17:25:26: Thread 15: starting\n17:25:26: Main    : create and start thread 16.\n17:25:26: Thread 16: starting\n17:25:26: Main    : create and start thread 17.\n17:25:26: Thread 17: starting\n17:25:26: Main    : create and start thread 18.\n17:25:26: Thread 18: starting\n17:25:26: Main    : create and start thread 19.\n17:25:26: Thread 19: starting\n17:25:26: Main    : create and start thread 20.\n17:25:26: Thread 20: starting\n17:25:26: Main    : create and start thread 21.\n17:25:26: Thread 21: starting\n17:25:26: Main    : create and start thread 22.\n17:25:26: Thread 22: starting\n17:25:26: Main    : create and start thread 23.\n17:25:26: Thread 23: starting\n17:25:26: Main    : create and start thread 24.\n17:25:26: Thread 24: starting\n17:25:26: Main    : create and start thread 25.\n17:25:26: Thread 25: starting\n17:25:26: Main    : create and start thread 26.\n17:25:26: Thread 26: starting\n17:25:26: Main    : create and start thread 27.\n17:25:26: Thread 27: starting\n17:25:26: Main    : create and start thread 28.\n17:25:26: Thread 28: starting\n17:25:26: Main    : create and start thread 29.\n17:25:26: Thread 29: starting\n17:25:26: Main    : create and start thread 30.\n17:25:26: Thread 30: starting\n17:25:26: Main    : create and start thread 31.\n17:25:26: Thread 31: starting\n17:25:26: Main    : before joining thread 0.\nStarted 0 at 1620651326.2126729488\nFinished 0 at 1620651336.2229239941\n17:25:36: Thread 0: finishing\n17:25:36: Main    : thread 0 done\n17:25:36: Main    : before joining thread 1.\nStarted 1 at 1620651327.2022259235\nFinished 1 at 1620651337.2120540142\n17:25:37: Thread 1: finishing\n17:25:37: Main    : thread 1 done\n17:25:37: Main    : before joining thread 2.\nStarted 30 at 1620651327.2101778984\nFinished 30 at 1620651337.2140960693\n17:25:37: Thread 30: finishing\nStarted 2 at 1620651328.2068090439\nFinished 2 at 1620651338.2182691097\n17:25:38: Thread 2: finishing\n17:25:38: Main    : thread 2 done\n17:25:38: Main    : before joining thread 3.\nStarted 31 at 1620651328.2222359180\nFinished 31 at 1620651338.2338199615\n17:25:38: Thread 31: finishing\nStarted 7 at 1620651329.2063989639\nFinished 7 at 1620651339.2115590572\n17:25:39: Thread 7: finishing\nStarted 15 at 1620651330.2087130547\nFinished 15 at 1620651340.2192440033\n17:25:40: Thread 15: finishing\nStarted 21 at 1620651331.2160348892\nFinished 21 at 1620651341.2246019840\n17:25:41: Thread 21: finishing\nStarted 23 at 1620651332.2160398960\nFinished 23 at 1620651342.2200219631\n17:25:42: Thread 23: finishing\nStarted 9 at 1620651333.2236700058\nFinished 9 at 1620651343.2359619141\n17:25:43: Thread 9: finishing\nStarted 18 at 1620651334.2257950306\nFinished 18 at 1620651344.2365601063\n17:25:44: Thread 18: finishing\nStarted 12 at 1620651335.2241439819\nFinished 12 at 1620651345.2335329056\n17:25:45: Thread 12: finishing\nStarted 20 at 1620651336.2342200279\nFinished 20 at 1620651346.2429049015\n17:25:46: Thread 20: finishing\nStarted 16 at 1620651337.4859669209\nFinished 16 at 1620651347.4899230003\n17:25:47: Thread 16: finishing\nStarted 22 at 1620651338.2339038849\nFinished 22 at 1620651348.2375440598\n17:25:48: Thread 22: finishing\nStarted 19 at 1620651339.2459530830\nFinished 19 at 1620651349.2504169941\n17:25:49: Thread 19: finishing\nStarted 13 at 1620651340.2416980267\nFinished 13 at 1620651350.2485001087\n17:25:50: Thread 13: finishing\nStarted 10 at 1620651340.2490129471\nFinished 10 at 1620651350.2568130493\n17:25:50: Thread 10: finishing\nStarted 29 at 1620651341.2439520359\nFinished 29 at 1620651351.2504179478\n17:25:51: Thread 29: finishing\nStarted 25 at 1620651342.2465701103\nFinished 25 at 1620651352.2498950958\n17:25:52: Thread 25: finishing\nStarted 17 at 1620651343.2493131161\nFinished 17 at 1620651353.2571830750\n17:25:53: Thread 17: finishing\nStarted 28 at 1620651344.2550890446\nFinished 28 at 1620651354.2586359978\n17:25:54: Thread 28: finishing\nStarted 14 at 1620651345.2569661140\nFinished 14 at 1620651355.2659308910\n17:25:55: Thread 14: finishing\nStarted 5 at 1620651346.2559928894\nFinished 5 at 1620651356.2631940842\n17:25:56: Thread 5: finishing\nStarted 4 at 1620651347.2538421154\nFinished 4 at 1620651357.2619009018\n17:25:57: Thread 4: finishing\nStarted 3 at 1620651347.2638580799\nFinished 3 at 1620651357.2686970234\n17:25:57: Thread 3: finishing\n17:25:57: Main    : thread 3 done\n17:25:57: Main    : before joining thread 4.\n17:25:57: Main    : thread 4 done\n17:25:57: Main    : before joining thread 5.\n17:25:57: Main    : thread 5 done\n17:25:57: Main    : before joining thread 6.\nStarted 26 at 1620651348.2553079128\nFinished 26 at 1620651358.2628009319\n17:25:58: Thread 26: finishing\nStarted 27 at 1620651348.2706210613\nFinished 27 at 1620651358.2781529427\n17:25:58: Thread 27: finishing\nStarted 24 at 1620651349.2586579323\nFinished 24 at 1620651359.2646100521\n17:25:59: Thread 24: finishing\nStarted 11 at 1620651350.2648739815\nFinished 11 at 1620651360.2702779770\n17:26:00: Thread 11: finishing\nStarted 8 at 1620651351.2621378899\nFinished 8 at 1620651361.2658278942\n17:26:01: Thread 8: finishing\nStarted 6 at 1620651352.4786870480\nFinished 6 at 1620651362.4896230698\n17:26:02: Thread 6: finishing\n17:26:02: Main    : thread 6 done\n17:26:02: Main    : before joining thread 7.\n17:26:02: Main    : thread 7 done\n17:26:02: Main    : before joining thread 8.\n17:26:02: Main    : thread 8 done\n17:26:02: Main    : before joining thread 9.\n17:26:02: Main    : thread 9 done\n17:26:02: Main    : before joining thread 10.\n17:26:02: Main    : thread 10 done\n17:26:02: Main    : before joining thread 11.\n17:26:02: Main    : thread 11 done\n17:26:02: Main    : before joining thread 12.\n17:26:02: Main    : thread 12 done\n17:26:02: Main    : before joining thread 13.\n17:26:02: Main    : thread 13 done\n17:26:02: Main    : before joining thread 14.\n17:26:02: Main    : thread 14 done\n17:26:02: Main    : before joining thread 15.\n17:26:02: Main    : thread 15 done\n17:26:02: Main    : before joining thread 16.\n17:26:02: Main    : thread 16 done\n17:26:02: Main    : before joining thread 17.\n17:26:02: Main    : thread 17 done\n17:26:02: Main    : before joining thread 18.\n17:26:02: Main    : thread 18 done\n17:26:02: Main    : before joining thread 19.\n17:26:02: Main    : thread 19 done\n17:26:02: Main    : before joining thread 20.\n17:26:02: Main    : thread 20 done\n17:26:02: Main    : before joining thread 21.\n17:26:02: Main    : thread 21 done\n17:26:02: Main    : before joining thread 22.\n17:26:02: Main    : thread 22 done\n17:26:02: Main    : before joining thread 23.\n17:26:02: Main    : thread 23 done\n17:26:02: Main    : before joining thread 24.\n17:26:02: Main    : thread 24 done\n17:26:02: Main    : before joining thread 25.\n17:26:02: Main    : thread 25 done\n17:26:02: Main    : before joining thread 26.\n17:26:02: Main    : thread 26 done\n17:26:02: Main    : before joining thread 27.\n17:26:02: Main    : thread 27 done\n17:26:02: Main    : before joining thread 28.\n17:26:02: Main    : thread 28 done\n17:26:02: Main    : before joining thread 29.\n17:26:02: Main    : thread 29 done\n17:26:02: Main    : before joining thread 30.\n17:26:02: Main    : thread 30 done\n17:26:02: Main    : before joining thread 31.\n17:26:02: Main    : thread 31 done\nTook 36.33210492134094\n#+end_example\n\n** Running in the Background\nThe =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.\n\n#+begin_src jupyter-python :kernel py_base :session emacs_py_1 :async yes :exports both\nfrom brish import z_background\n\nmsg = \"You can’t make an omelet without breaking a few eggs.\"\nresult_future = z_background(\n    \"say {msg}\",\n    # Needs the `say` command, available by default on macOS\n)\nresult_future\n#+end_src\n\n#+RESULTS:\n: \u003cFuture at 0x1435b51e0 state=running\u003e\n\nThe =z_background= function returns a =Future= object.\n\n#+begin_src jupyter-python :kernel py_base :session emacs_py_1 :async yes :exports both\nresult_future.result()\n#+end_src\n\n#+RESULTS:\n: CmdResult(retcode=0, out='', err='', cmd=\" say 'You can’t make an omelet without breaking a few eggs.' \", cmd_stdin='')\n\nHere is another example:\n\n#+begin_src jupyter-python :kernel py_base :session emacs_py_1 :async yes :exports both\nfrom brish import z_background\nimport concurrent.futures\n\n# Define multiple commands\ncommands = [\n    \"sleep 5 \u0026\u0026 echo 'First command completed.'\",\n    \"sleep 3 \u0026\u0026 echo 'Second command completed.'\",\n    \"sleep 1 \u0026\u0026 echo 'Third command completed.'\",\n]\n\n# Execute all commands asynchronously\nfutures = [z_background(cmd) for cmd in commands]\n\nfor future in concurrent.futures.as_completed(futures):\n    result = future.result()\n    print(result.out)\n#+end_src\n\n#+RESULTS:\n: Third command completed.\n: \n: Second command completed.\n: \n: First command completed.\n: \n\n\n* Security Considerations\nI 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.\n\nNote that you can create security holes for yourself, by, e.g., running =eval= on user input:\n\n#+begin_src python :session p1 :results value :exports both\nuntrusted_input = \" ; echo do evil | cat\"\nz(\"eval {untrusted_input}\") # unsafe\n#+end_src\n\n#+RESULTS:\n#+begin_example\ndo evil\n#+end_example\n\n#+begin_src python :session p1 :results value :exports both\nz(\"echo {untrusted_input}\") # safe\n#+end_src\n\n#+RESULTS:\n#+begin_example\n ; echo do evil | cat\n#+end_example\n\n# One thing to keep in mind is that Brish purposely uses the zsh from your PATH. That zsh will load its dotfiles as usual.\n\n* Known Issues\n- Piping binary (non-text) output from zsh to Python does not work\n\n- Nonstandard encodings (non UTF-8) are corrupted\n  #+begin_src python :session p1 :results value :exports both\n  z(\"echo 'sth × another (ver.-)'\")\n  #+end_src\n\n  #+RESULTS:\n  #+begin_example\n  sth Ã\\xb7 another (ver.-)\n  #+end_example\n\n- 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 =\u003c/dev/null= or =\u003c\u0026-= can be a suitable workaround.\n\n* Future Features\nI 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~.\n\nIf you have any good design ideas, create an issue!\n\n* Related Projects\n- [[https://github.com/sharkdp/pysh][pysh]] uses comments in bash scripts to switch the interpreter to Python, allowing variable reuse between the two.\n- [[https://github.com/tomerfiliba/plumbum][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.\n- [[https://github.com/timofurrer/shellfuncs][shellfuncs]]: Python API to execute shell functions as they would be Python functions. (Last commit is in 2017.)\n- [[https://github.com/xonsh/xonsh][xonsh]] is a superset of Python 3.5+ with additional shell primitives.\n- [[https://github.com/terrycojones/daudin][daudin]] [[https://github.com/terrycojones/daudin#how-commands-are-interpreted][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 [[https://github.com/terrycojones/daudin/issues/11][think]] it can use Brish to solve this, but someone needs to contribute the support.\n- [[https://github.com/oconnor663/duct.py][duct.py]] is a library for running child processes. It's quite low-level compared to the other projects in this list.\n- ~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:\n    #+BEGIN_EXAMPLE\n    alias x='noglob calc-raw'\n    calc-raw () {\n        python3 -c \"from math import *; print($*)\"\n    }\n    #+END_EXAMPLE\n- [[https://github.com/danylo-dubinin/zsh-jupyter-kernel][Z shell kernel for Jupyter Notebook]] allows you to do all sorts of stuff if you spend the time implementing your usecase; See [[https://github.com/nnicandro/emacs-jupyter#org-mode-source-blocks][emacs-jupyter]] to get a taste of what's possible. [[https://github.com/jupyter/kernel_gateway][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 [[https://github.com/Valodim/zsh-capture-completion][thingy]] that was very buggy when I tried it.\n- Finally, if you're feeling adventurous, try Rust's [[https://github.com/rust-shell-script/rust_cmd_lib][rust_cmd_lib]]. It's quite beautiful.\n\n* Licenses\nDual-licensed under MIT and GPL v3 or later.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNightMachinery%2Fbrish","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FNightMachinery%2Fbrish","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNightMachinery%2Fbrish/lists"}