{"id":15021638,"url":"https://github.com/adamhl8/shellrunner","last_synced_at":"2025-03-30T20:32:10.388Z","repository":{"id":143400543,"uuid":"612327988","full_name":"adamhl8/shellrunner","owner":"adamhl8","description":"Write safe shell scripts in Python.","archived":false,"fork":false,"pushed_at":"2024-02-29T19:15:00.000Z","size":81,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-13T00:44:44.353Z","etag":null,"topics":["bash","bash-scripting","python","python-scripting","shell","shell-script","shell-scripting","shell-scripts"],"latest_commit_sha":null,"homepage":"","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/adamhl8.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,"publiccode":null,"codemeta":null}},"created_at":"2023-03-10T17:39:16.000Z","updated_at":"2024-06-24T19:29:15.000Z","dependencies_parsed_at":"2023-11-17T04:32:05.649Z","dependency_job_id":"f17416a5-5ac0-4c63-8e83-92f56d8e7219","html_url":"https://github.com/adamhl8/shellrunner","commit_stats":{"total_commits":46,"total_committers":1,"mean_commits":46.0,"dds":0.0,"last_synced_commit":"cd12776fa90d1aca881f4b3fde480e2d4674feb7"},"previous_names":["adamhl8/shellrunner"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fshellrunner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fshellrunner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fshellrunner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fshellrunner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamhl8","download_url":"https://codeload.github.com/adamhl8/shellrunner/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222112054,"owners_count":16933464,"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":["bash","bash-scripting","python","python-scripting","shell","shell-script","shell-scripting","shell-scripts"],"created_at":"2024-09-24T19:56:49.587Z","updated_at":"2024-10-29T21:09:49.878Z","avatar_url":"https://github.com/adamhl8.png","language":"Python","readme":"# ShellRunner\n\n\u003cdiv align=\"center\"\u003e\n  \u003cimg width=\"100\" src=\"https://github.com/adamhl8/python-shellrunner/assets/1844269/035c8ae0-bb77-473b-ac1c-2bc83ce749ac\"\u003e\n\u003c/div\u003e\n\u003cp align=\"center\"\u003e\n  Write safe shell scripts in Python.\n  \u003cbr\u003e\n  Combine the streamlined utility of a shell with the power of a modern programming language.\n\u003c/p\u003e\n\n---\n\n- [Install](#install)\n- [Usage](#usage)\n- [Why?](#why)\n  - [Similar Projects](#similar-projects)\n- [Advanced Usage](#advanced-usage)\n  - [Shell Command Result](#shell-command-result)\n  - [Exception Handling](#exception-handling)\n  - [Multiple Commands / Persisting Environment](#multiple-commands--persisting-environment)\n- [Options](#options)\n  - [Output](#output)\n  - [Environment Variables](#environment-variables)\n- [Examples](#examples)\n\n## Install\n\n```\npip install -U shellrunner\n```\n\n## Usage\n\n```python\nfrom shellrunner import X\n\nX(\"echo hello world\")\n# hello world\n```\n\nEasily get a command's output, do something with it, and run another command using the value:\n\n```python\noutput = X(\"echo hello world | sed 's/world/there/'\").out\ngreeting = output.capitalize()\nX(f\"echo 'echo {greeting}' \u003e\u003e .bashrc\")\n```\n\nAn exception is raised if a command exits with a non-zero status (like bash's `set -e`):\n\n```python\ntext = X(\"grep hello /non/existent/file\").out # grep exits with a non-zero status\n# ^ Raises ShellCommandError so the rest of the script doesn't run\nmy_text_processor(text)\n```\n\nOr, maybe you want to handle the error:\n\n```python\nfrom shellrunner import X, ShellCommandError\n\ntext = \"\"\ntry:\n    text = X(\"grep hello /non/existent/file\").out\nexcept ShellCommandError:\n    text = X(\"grep hello /file/that/definitely/exists\").out\nmy_text_processor(text)\n```\n\nPipeline errors are not masked (like bash's `set -o pipefail`):\n\n```python\nX(\"grep hello /non/existent/file | tee new_file\") # tee gets nothing from grep, creates an empty file, and exits with status 0\n# ^ Raises ShellCommandError\n```\n\n## Why?\n\n\u003e Why not just use bash with `set -e` and `set -o pipefail`?\n\nBecause writing anything remotely complicated in bash kinda sucks :)\n\nOne of the primary advantages of ShellRunner's approach is that you can seamlessly swap between the shell and Python. Some things are just easier to do in a shell (e.g. pipelines) and a lot of things are easier/better in Python (control flow, error handling, etc).\n\nAlso, users of [fish](https://github.com/fish-shell/fish-shell) might know that it [does not offer a way to easily exit a script if a command fails](https://github.com/fish-shell/fish-shell/issues/510). ShellRunner adds `set -e` and `pipefail` like functionality to any shell. Leverage the improved syntax of your preferred shell and the (optional) safety of bash.\n\n### Similar Projects\n\n- [zxpy](https://github.com/tusharsadhwani/zxpy)\n- [shellpy](https://github.com/lamerman/shellpy)\n- [plumbum](https://github.com/tomerfiliba/plumbum)\n\nShellRunner is very similar to zxpy and shellpy but aims to be more simple in its implementation and has a focus on adding safety to scripts.\n\n## Advanced Usage\n\nA note on compatibility: ShellRunner should work with on any POSIX-compliant system (and shell). No Windows support at this time.\n\nConfirmed compatible with `sh` (dash), `bash`, `zsh`, and `fish`.\n\nCommands are automatically run with the shell that invoked your python script (this can be [overridden](#options)):\n\n```python\n# my_script.py\nX(\"echo hello | string match hello\")\n# Works if my_script.py is executed under fish (string match). Will obviously fail if using bash.\n```\n\n### Shell Command Result\n\n`X` returns a `ShellCommandResult` (`NamedTuple`) containing the following:\n\n- `out: str`: The `stdout` and `stderr` of the command.\n- `status: int`: The overall exit status of the command. If the command was a pipeline that failed, `status` will be equal to the status of the last failing command (like bash's `pipefail`).\n- `pipestatus: list[int]`: A list of statuses for each command in the pipeline.\n\n```python\nresult = X(\"echo hello\")\nprint(f'Got output \"{result.out}\" with exit status {result.status} / {result.pipestatus}')\n# Or unpack\noutput, status, pipestatus = X(\"echo hello\")\n# output = \"hello\"\n# status = 0\n# pipestatus = [0]\n```\n\n```python\nresult = X(\"(exit 1) | (exit 2) | echo hello\")\n# result.out = \"hello\"\n# result.status = 2\n# result.pipestatus = [1, 2, 0]\n```\n\nIf using a shell that does not support `PIPESTATUS` such as `sh`, you will only ever get the status of the last command in a pipeline. **This also means that in this case ShellRunner cannot detect if an error occurred in a pipeline:**\n\n```python\nresult = X(\"(exit 1) | echo hello\")\n# if invoked with bash: ShellCommandError is raised, status = 1, pipestatus = [1, 0]\n# if invoked with sh: No exception is raised, status = 0, pipestatus = [0]\n```\n\n### Exception Handling\n\n`ShellCommandError` also receives the information from the failed command, which means you can do something like this:\n\n```python\ntry:\n    X(\"echo hello \u0026\u0026 false\") # Pretend this is some command that prints something but exits with a non-zero status\nexcept ShellCommandError as e:\n    print(f'Command failed. Got output \"{e.out}\" with exit status {e.status}')\n```\n\n### Multiple Commands / Persisting Environment\n\nEach call of `X` invokes a new instance of the shell, so things like environment variables or directory changes don't persist.\n\nSometimes you might want to do something like this:\n\n```python\nX(\"MY_VAR=hello\")\nX(\"grep $MY_VAR /file/that/exists\") # MY_VAR doesn't exist\n# ^ Raises ShellCommandError\n```\n\nA (bad) solution would be to do this:\n\n```python\nX(\"MY_VAR=hello; grep $MY_VAR /file/that/exists\")\n```\n\nThis sort of defeats the purpose of ShellRunner because that would be run as one command, so no error handling can take place on commands before the last one.\n\nInstead, `X` also accepts a list of commands where each command is run in the same shell instance and goes through the normal error handling:\n\n```python\nX([\n\"MY_VAR=hello\",\n\"grep $MY_VAR /file/that/exists\",\n])\n# Works!\n```\n\n## Options\n\nThere are a few keyword arguments you can provide to adjust the behavior of `X`:\n\n```python\nX(\"command\", shell=\"bash\", check=True, show_output=True, show_command=True)\n```\n\n`shell: str` (Default: the invoking shell) - Shell that will be used to execute the commands. Can be a path or simply the name (e.g. \"/bin/bash\", \"bash\").\n\n`check: bool` (Default: True) - If True, an error will be thrown if a command exits with a non-zero status.\n\n`show_output: bool` (Default: True) - If True, command output will be printed.\n\n`show_command: bool` (Default: True) - If True, the current command will be printed before execution.\n\n### Output\n\nSay you do this:\n\n```python\nX(\"echo hello world\")\n```\n\nThis will print the following to your terminal:\n\n```\nshellrunner: echo hello world\nhello world\n```\n\nTo hide the `shellrunner:` lines, set `show_command=False`.\n\nTo hide actual command output, set `show_output=False`.\n\n### Environment Variables\n\nEach option also has a corresponding environment variable to allow you to set these options \"globally\" for your script:\n\n`shell` = `SHELLRUNNER_SHELL`\n\n`check` = `SHELLRUNNER_CHECK`\n\n`show_output` = `SHELLRUNNER_SHOW_OUTPUT`\n\n`show_command` = `SHELLRUNNER_SHOW_COMMAND`\n\nEnvironment variables are evaluated on each call of `X`, so you could also do something like this:\n\n```python\n# Pretend that before running this file you set: export SHELLRUNNER_SHOW_OUTPUT=\"False\"\nX(\"echo hello\")\n# No output\n\n# Now you want to see output\nos.environ[\"SHELLRUNNER_SHOW_OUTPUT\"] = \"True\"\nX(\"echo hello\")\n# hello\n```\n\n## Examples\n\nPrints out installed python packages and their dependencies:\n\n```python\nfrom shellrunner import X\n\npackages = X(\"pip list -l | sed 1,2d | awk '{print $1}'\").out\npackages = packages.splitlines()\n\nfor package in packages:\n    print(f\"=== {package} ===\")\n    X(f\"pip show {package} | grep -E 'Requires|Required-by'\", show_command=False)\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamhl8%2Fshellrunner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamhl8%2Fshellrunner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamhl8%2Fshellrunner/lists"}