python-shellrunner


Namepython-shellrunner JSON
Version 0.3.5 PyPI version JSON
download
home_page
SummaryWrite safe shell scripts in Python.
upload_time2023-10-18 20:33:01
maintainer
docs_urlNone
author
requires_python>=3.10
licenseMIT
keywords bash fish scripting shell zsh
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <div align="center">
  <img width="250" src="https://user-images.githubusercontent.com/1844269/226196799-402898d6-c363-4735-be23-57c0ba9e1035.png">
</div>
<br>
<p align="center">
  Write safe shell scripts in Python.
  <br>
  Combine the streamlined utility of a shell with the power of a modern programming language.
</p>

---

- [Install](#install)
- [Usage](#usage)
- [Why?](#why)
  - [Similar Projects](#similar-projects)
- [Advanced Usage](#advanced-usage)
  - [Shell Command Result](#shell-command-result)
  - [Exception Handling](#exception-handling)
  - [Multiple Commands / Persisting Environment](#multiple-commands--persisting-environment)
- [Options](#options)
  - [Output](#output)
  - [Environment Variables](#environment-variables)
- [Examples](#examples)

## Install

```
pip install -U python-shellrunner
```

## Usage

```python
from shellrunner import X

X("echo hello world")
# hello world
```

Easily get a command's output, do something with it, and run another command using the value:

```python
output = X("echo hello world | sed 's/world/there/'").out
greeting = output.capitalize()
X(f"echo 'echo {greeting}' >> .bashrc")
```

An exception is raised if a command exits with a non-zero status (like bash's `set -e`):

```python
text = X("grep hello /non/existent/file").out # grep exits with a non-zero status
# ^ Raises ShellCommandError so the rest of the script doesn't run
my_text_processor(text)
```

Or, maybe you want to handle the error:

```python
from shellrunner import X, ShellCommandError

text = ""
try:
    text = X("grep hello /non/existent/file").out
except ShellCommandError:
    text = X("grep hello /file/that/definitely/exists").out
my_text_processor(text)
```

Pipeline errors are not masked (like bash's `set -o pipefail`):

```python
X("grep hello /non/existent/file | tee new_file") # tee gets nothing from grep, creates an empty file, and exits with status 0
# ^ Raises ShellCommandError
```

## Why?

> Why not just use bash with `set -e` and `set -o pipefail`?

Because writing anything remotely complicated in bash kinda sucks :)

One 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).

Also, 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) saftey of bash.

### Similar Projects

- [zxpy](https://github.com/tusharsadhwani/zxpy)
- [shellpy](https://github.com/lamerman/shellpy)
- [plumbum](https://github.com/tomerfiliba/plumbum)

ShellRunner 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.

## Advanced Usage

A note on compatability: ShellRunner should work with on any POSIX-compliant system (and shell). No Windows support at this time.

Confirmed compatible with `sh` (dash), `bash`, `zsh`, and `fish`.

Commands are automatically run with the shell that invoked your python script (this can be [overridden](#options)):

```python
# my_script.py
X("echo hello | string match hello")
# Works if my_script.py is executed under fish. Will obviously fail if using bash.
```

### Shell Command Result

`X` returns a `ShellCommandResult` (`NamedTuple`) containing the following:

- `out: str`: The `stdout` and `stderr` of the command.
- `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`).
- `pipestatus: list[int]`: A list of statuses for each command in the pipeline.

```python
result = X("echo hello")
print(f'Got output "{result.out}" with exit status {result.status} / {result.pipestatus}')
# Or unpack
output, status, pipestatus = X("echo hello")
# output = "hello"
# status = 0
# pipestatus = [0]
```

```python
result = X("(exit 1) | (exit 2) | echo hello")
# result.out = "hello"
# result.status = 2
# result.pipestatus = [1, 2, 0]
```

If 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:**

```python
result = X("(exit 1) | echo hello")
# if invoked with bash: ShellCommandError is raised, status = 1, pipestatus = [1, 0]
# if invoked with sh: No exception is raised, status = 0, pipestatus = [0]
```

### Exception Handling

`ShellCommandError` also receives the information from the failed command, which means you can do something like this:

```python
try:
    X("echo hello && false") # Pretend this is some command that prints something but exits with a non-zero status
except ShellCommandError as e:
    print(f'Command failed. Got output "{e.out}" with exit status {e.status}')
```

### Multiple Commands / Persisting Environment

Each call of `X` invokes a new instance of the shell, so things like environment variables or directory changes don't persist.

Sometimes you might want to do something like this:

```python
X("MY_VAR=hello")
X("grep $MY_VAR /file/that/exists") # MY_VAR doesn't exist
# ^ Raises ShellCommandError
```

A (bad) solution would be to do this:

```python
X("MY_VAR=hello; grep $MY_VAR /file/that/exists")
```

This 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.

Instead, `X` also accepts a list of commands where each command is run in the same shell instance and goes through the normal error handling:

```python
X([
"MY_VAR=hello",
"grep $MY_VAR /file/that/exists",
])
# Works!
```

## Options

There are a few keyword arguments you can provide to adjust the behavior of `X`:

```python
X("command", shell="bash", check=True, show_output=True, show_commands=True)
```

`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").

`check: bool` (Default: True) - If True, an error will be thrown if a command exits with a non-zero status.

`show_output: bool` (Default: True) - If True, command output will be printed.

`show_commands: bool` (Default: True) - If True, the current command will be printed before execution.

### Output

Say you do this:

```python
X("echo hello world")
```

This will print the following to your terminal:

```
shellrunner: echo hello world
hello world
```

To hide the `shellrunner:` lines, set `show_commands=False`.

To hide actual command output, set `show_output=False`.

### Environment Variables

Each option also has a corresponding environment variable to allow you to set these options "globally" for your script:

`shell` = `SHELLRUNNER_SHELL`

`check` = `SHELLRUNNER_CHECK`

`show_output` = `SHELLRUNNER_SHOW_OUTPUT`

`show_commands` = `SHELLRUNNER_SHOW_COMMANDS`

Environment variables are evaluated on each call of `X`, so you could also do something like this:

```python
# Pretend that before running this file you set: export SHELLRUNNER_SHOW_OUTPUT="False"
X("echo hello")
# No output

# Now you want to see output
os.environ["SHELLRUNNER_SHOW_OUTPUT"] = "True"
X("echo hello")
# hello
```

## Examples

Prints out installed python packages and their dependencies:

```python
from shellrunner import X

packages = X("pip list -l | sed 1,2d | awk '{print $1}'").out
packages = packages.splitlines()

for package in packages:
    print(f"=== {package} ===")
    X(f"pip show {package} | grep -E 'Requires|Required-by'", show_commands=False)
```

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "python-shellrunner",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": "",
    "keywords": "bash,fish,scripting,shell,zsh",
    "author": "",
    "author_email": "adamhl8 <adamhl@pm.me>",
    "download_url": "https://files.pythonhosted.org/packages/eb/dc/23caf7cbebe3f74d3d4db3a5530a8088cc4dfc549fb3639311cd295b4233/python_shellrunner-0.3.5.tar.gz",
    "platform": null,
    "description": "<div align=\"center\">\n  <img width=\"250\" src=\"https://user-images.githubusercontent.com/1844269/226196799-402898d6-c363-4735-be23-57c0ba9e1035.png\">\n</div>\n<br>\n<p align=\"center\">\n  Write safe shell scripts in Python.\n  <br>\n  Combine the streamlined utility of a shell with the power of a modern programming language.\n</p>\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 python-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}' >> .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> 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) saftey 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 compatability: 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. 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 && 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_commands=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_commands: 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_commands=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_commands` = `SHELLRUNNER_SHOW_COMMANDS`\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_commands=False)\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Write safe shell scripts in Python.",
    "version": "0.3.5",
    "project_urls": {
        "Bug Tracker": "https://github.com/adamhl8/python-shellrunner/issues",
        "Homepage": "https://github.com/adamhl8/python-shellrunner",
        "Source": "https://github.com/adamhl8/python-shellrunner"
    },
    "split_keywords": [
        "bash",
        "fish",
        "scripting",
        "shell",
        "zsh"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "53c7bb17e5697d1c1daab7277328fff760d1eaec5cbef041b423b42aba3da838",
                "md5": "f04b482466b5a0e71ef72cf668355e97",
                "sha256": "a44f2cea4512425b20ab522c07c10da53d3757c6b4da2c797de93f0fcd88efd0"
            },
            "downloads": -1,
            "filename": "python_shellrunner-0.3.5-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "f04b482466b5a0e71ef72cf668355e97",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 10549,
            "upload_time": "2023-10-18T20:32:59",
            "upload_time_iso_8601": "2023-10-18T20:32:59.688398Z",
            "url": "https://files.pythonhosted.org/packages/53/c7/bb17e5697d1c1daab7277328fff760d1eaec5cbef041b423b42aba3da838/python_shellrunner-0.3.5-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "ebdc23caf7cbebe3f74d3d4db3a5530a8088cc4dfc549fb3639311cd295b4233",
                "md5": "5c877c2deb98167bf37e3e6b05389b87",
                "sha256": "706219bde53ec7f3cdbac8a2658da13d1e73914a6e4d828e3aca3634cb5f0a25"
            },
            "downloads": -1,
            "filename": "python_shellrunner-0.3.5.tar.gz",
            "has_sig": false,
            "md5_digest": "5c877c2deb98167bf37e3e6b05389b87",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 9188,
            "upload_time": "2023-10-18T20:33:01",
            "upload_time_iso_8601": "2023-10-18T20:33:01.286970Z",
            "url": "https://files.pythonhosted.org/packages/eb/dc/23caf7cbebe3f74d3d4db3a5530a8088cc4dfc549fb3639311cd295b4233/python_shellrunner-0.3.5.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-18 20:33:01",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "adamhl8",
    "github_project": "python-shellrunner",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "python-shellrunner"
}
        
Elapsed time: 0.31368s