{"id":24388216,"url":"https://github.com/frewtypebbles/shelltool","last_synced_at":"2026-04-21T12:35:13.022Z","repository":{"id":234854565,"uuid":"789634180","full_name":"FrewtyPebbles/Shelltool","owner":"FrewtyPebbles","description":null,"archived":false,"fork":false,"pushed_at":"2024-04-24T02:10:05.000Z","size":26,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-05T04:34:46.906Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/FrewtyPebbles.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2024-04-21T05:35:47.000Z","updated_at":"2024-04-28T02:43:52.000Z","dependencies_parsed_at":"2024-04-21T06:23:59.401Z","dependency_job_id":null,"html_url":"https://github.com/FrewtyPebbles/Shelltool","commit_stats":null,"previous_names":["frewtypebbles/shellify.py"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrewtyPebbles%2FShelltool","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrewtyPebbles%2FShelltool/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrewtyPebbles%2FShelltool/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/FrewtyPebbles%2FShelltool/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/FrewtyPebbles","download_url":"https://codeload.github.com/FrewtyPebbles/Shelltool/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243294929,"owners_count":20268308,"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":[],"created_at":"2025-01-19T13:56:41.115Z","updated_at":"2025-12-14T19:02:02.693Z","avatar_url":"https://github.com/FrewtyPebbles.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shelltool\n\nShelltool is an api that makes dealing with and composing subprocesses in python easier, more readable, and more immediately useful.  It accomplishes this by utilizing syntax that makes it feel more like composing procedures in Bash rather than dealing with things like `Popen` or `Thread(target=lambda:subprocess.run())`.\n\nHeres an example of the syntax.  \n```py\nif __name__ == \"__main__\":\n\n    process = ~(SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\"))\n    \n    # This is the same as running the following command on a separate thread:\n    # cat \"./shelltool.py\" | grep \"SHELL\"\n\n    process.run()\n    # .run() starts the process, or in this case the thread with running the process\n\n    # The process joins the current thread when the attributes `stdout` or `stderr` are accessed\n    print(f\"result:\\n{process.stdout.decode()}\")\n```\n\n# How to Shelltool Your Python\n\nShelltool has a couple of syntax and operators that may look familiar for helping you write quick and functional Bash-like code.\n\n## Composing a Command\n\nTo compose a command with Shelltool, first import the `SHELL`:\n\n```py\nfrom shelltool import SHELL\n```\n\nNext, choose an executable or command to run:\n\n```py\nSHELL.grep\n# or\nSHELL[\"grep\"]\n# or\nSHELL[\"/path/to/grep binary\"]\n```\n\nNext, pass your arguments to your executable or command:\n\n```py\nSHELL.grep(\"SHELL\", \"./shelltool.py\")\n# or\nSHELL[\"grep\"](\"SHELL\", \"./shelltool.py\")\n# or\nSHELL[\"/path/to/grep binary\"](\"SHELL\", \"./shelltool.py\")\n```\n\nThen, run your shell executable or command:\n\n```py\nSHELL.grep(\"SHELL\", \"./shelltool.py\").run()\n# or\nSHELL[\"grep\"](\"SHELL\", \"./shelltool.py\").run()\n# or\nSHELL[\"/path/to/grep binary\"](\"SHELL\", \"./shelltool.py\").run()\n```\n\nFinally, get all the data you need from your executable or command:\n\n```py\ngrep_cmd = SHELL.grep(\"SHELL\", \"./shelltool.py\").run()\n# or\ngrep_cmd = SHELL[\"grep\"](\"SHELL\", \"./shelltool.py\").run()\n# or\ngrep_cmd = SHELL[\"/path/to/grep binary\"](\"SHELL\", \"./shelltool.py\").run()\n\ngrep_cmd.stdout # the stdout of your process\ngrep_cmd.stderr # the stderr of your process\ngrep_cmd.pid # the pid of your process\n```\n\n## The Pipe Operators\n\n`|` and `@`\n\nThe *Pipe* operators work the same as how they do in Bash:\n\nThe `|` operator is the same as `|` in Bash.  It pipes the stdout of the left hand side of the operator into the stdin of the right hand side of the operator.\n\n```py\ncat_to_grep_cmd = SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\")\ncat_to_grep_cmd.run()\n\nprint(cat_to_grep_cmd.stdout.decode())\n```\n\nIt works with any datatype on the left hand side that has a `__str__` dunder.\n\n```py\ncat_to_grep_cmd = \"SHELL is\\nso\\nCOOL!\" | SHELL.grep(\"SHELL\")\ncat_to_grep_cmd.run()\n\nprint(cat_to_grep_cmd.stdout.decode())\n```\n\nThe `@` operator pipes the stderr of the left hand side of the operator into the stdin of the right hand side of the operator.\n\n```py\ncat_to_grep_cmd = SHELL.SHELL() @ SHELL.grep(\"SHELL\")\ncat_to_grep_cmd.run()\n\nprint(cat_to_grep_cmd.stdout.decode())\n```\n\n## The Tilde Operator\n\n`~`\n\nThe *Tilde* operator runs the supplied process on a separate thread:\n\n```py\n# Lets pretend we need to run a slow subprocess.\n\n# By just adding a tilde, we can instantly move this subprocess to a separate concurrent thread.\ncat_to_grep_cmd = ~(SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\"))\n\n# Now lets run our slow subprocess/spawn our thread.\ncat_to_grep_cmd.run()\n\n# Now that our process is happening off of the main thread we can do other computations while we wait for it to finish\nwhile cat_to_grep_cmd.running:\n    # do some other tasks...\n    print(f\"Currently doing concurrent tasks while running subprocess with pid: {cat_to_grep_cmd.pid}\")\n\n# Finally we've finished our other tasks, so lets get our long awaited stdout and stderr data from our subprocess.  Accessing either stdout or stderr on our process will join our thread back to its spawning thread, or in this case the main thread.\nprint(cat_to_grep_cmd.stdout.decode())\nprint(cat_to_grep_cmd.stderr.decode())\n```\n\n## The \u0026 Operator\n\n`\u0026`\n\nThe *\u0026* operator runs process on its left hand side before its process on its right hand side:\n\n```py\ncat_to_grep_cmd = ~(SHELL.sleep(5) \u0026 SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\"))\n# This will sleep 5 seconds before calling `cat ./shelltool.py | grep shell`\n\ncat_to_grep_cmd.run()\n\n# Now we can do some other tasks while the the terminal is sleeping then cat-ing to grep-ing\nwhile cat_to_grep_cmd.running:\n    # do some other tasks...\n    print(f\"Currently doing concurrent tasks while running subprocess with pid: {cat_to_grep_cmd.pid}\")\n\n# Finally we've finished our other tasks, so lets get our long awaited stdout and stderr data from our subprocess.  Accessing either stdout or stderr on our process will join our thread back to its spawning thread, or in this case the main thread.\nprint(cat_to_grep_cmd.stdout.decode())\nprint(cat_to_grep_cmd.stderr.decode())\n```\n\n## But how do I get my stdout and stderr as my program runs?\n\n`process_variable.next_stream_line()`\n\n`.next_stream_line()` grabs the next line of stdout and stderr as they are emitted from the process in real time.  It returns these lines as a tuple of bytes `(stdout:bytes | None, stderr:bytes | None)`.  You can check if there are any available lines of stdout and stderr from your process with the boolean property `process.stream_empty`.\n\nexample:\n\nLets say we have the following python program `test.py`:\n\n```py\nfrom time import sleep\nimport sys\nfor i in range(10):\n    if i % 2 == 0:\n        print(i, file=sys.stderr)\n    else:\n        print(i)\n    sleep(0.5)\n```\n\nNow here is our code running our python program:\n\n```py\nprocess = ~(SHELL.python(\"-u\", \"test.py\"))\n# This runs test.py with unbuffered writes to IO\n\nprocess.run()\n\nwhile process.running or not process.stream_empty:\n    # while the process is running or there are stdouts and stderrs left from our process.\n\n    out, err = process.next_stream_line() #grab any potential incoming stdout and stderr\n\n    if out: # if stdout line is not none:\n        print(f\"PROCESS RUNNING: {process.running}\")\n        sys.stdout.write(f\"out:\\n{out.decode()}\")\n\n    if err: # if stderr line is not none:\n        print(f\"PROCESS RUNNING: {process.running}\")\n        sys.stdout.write(f\"err:\\n{err.decode()}\")\n```\n\n***Incase anyone needs to know***: \n\nThis function itterates to the next output buffer in the stream.  So if you want to itterate through each write to the stdout, you will need to make sure your writes are unbuffered.\n\n## Help! My subprocess is out of controll! (How to Kill Your Subprocess) \n\nKilling your rogue subprocess is as simple as `.kill()`.\n\n```py\ncat_to_grep_cmd = ~(SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\"))\ncat_to_grep_cmd.run()\n\ncat_to_grep_cmd.kill() # RIP subprocess ;(\n\nprint(cat_to_grep_cmd.stdout.decode())\n```\n\n## What If I Want My Subprocess's pid?\n\nGetting a subprocess's pid is as simple as `.pid`.\n\n```py\ncat_to_grep_cmd = ~(SHELL.cat(\"./shelltool.py\") | SHELL.grep(\"SHELL\"))\ncat_to_grep_cmd.run()\n\nprint(cat_to_grep_cmd.pid) # Here it is!\n\nprint(cat_to_grep_cmd.stdout.decode())\n```\n\n## Uh Oh! Race conditions! (How To Join a Concurrent Subprocess Back To It's Spawning Thread)\n\nTo join a subprocess back to its spawning thread call `.finish()`:\n```py\nprocess, process_err = ~((p1 := (SHELL.echo(\"SHELL\") | SHELL.tee(\"/dev/stderr\"))) | SHELL.grep(\"SHELL\")), ~(p1 @ SHELL.grep(\"SHELL\"))\nprocess.run()\n\nprocess.finish()\n# this joins the thread/process so `p1` and the rest of `process` is evaluated before `p1` is used to evaluate `process_err`.\n\nprocess_err.run()\n\nprint(f\"p_out:\\n{process.stdout.decode()}\")\nprint(f\"p_err:\\n{process_err.stdout.decode()}\")\n```\n\n***Side note***, you could also fix this specific race condition without calling `.finish()` by running `p1` before `process` and `process_err`:\n\n```py\nprocess, process_err = ~((p1 := (SHELL.echo(\"SHELL\") | SHELL.tee(\"/dev/stderr\"))) | SHELL.grep(\"SHELL\")), ~(p1 @ SHELL.grep(\"SHELL\"))\np1.run()\nprocess.run()\nprocess_err.run()\n\nprint(f\"p_out:\\n{process.stdout.decode()}\")\nprint(f\"p_err:\\n{process_err.stdout.decode()}\")\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrewtypebbles%2Fshelltool","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrewtypebbles%2Fshelltool","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrewtypebbles%2Fshelltool/lists"}