sh: Python Subprocess Replacement
sh is a full-fledged subprocess replacement for Python (3.8 - 3.12) that allows you to call any program on your system as if it were a function. It dynamically resolves programs from your `$PATH`, similar to Bash, and wraps them in callable Python functions, offering an intuitive way to write shell scripts in Python. The current version is 2.2.2, and it maintains an active release cadence with regular bug fixes and minor feature additions.
Warnings
- breaking The `sh` library is designed exclusively for Unix-like operating systems (Linux, macOS, BSDs). It relies on various Unix system calls and does not support Windows environments.
- breaking Upgrading from `sh` 1.x to 2.x involves significant breaking changes. Users are strongly advised to consult the `MIGRATION.md` file in the project's repository or PyPI page before upgrading.
- gotcha When an executed command exits with a non-zero status code, `sh` raises an `sh.ErrorReturnCode_X` exception (where X is the exit code), rather than silently failing. This behavior must be explicitly handled with `try...except` blocks.
- gotcha Prior to version 2.2.2, using `async` commands in combination with `_return_cmd=True` could lead to unexpected behavior. Specifically, `await` on such a command might return `str(self)` instead of a `RunningCommand` object, and earlier versions (pre-2.2.1) had bugs where async commands with `return_cmd` did not raise exceptions correctly.
- gotcha While `sh` provides a Pythonic wrapper, it directly executes underlying shell commands. This means many common shell footguns (e.g., issues with improper quoting, globbing, or special characters in filenames) can still affect your scripts.
Install
-
pip install sh
Imports
- sh
import sh
- Command
from sh import Command
- ErrorReturnCode
from sh import ErrorReturnCode_127
- bash
from sh.contrib import bash
Quickstart
import sh
import os
# Get the current user
user = os.environ.get('USER', 'unknown_user')
print(f"Hello, {user}!")
# Run a simple command and capture output
output = sh.ls('-l', '/tmp')
print(f"\nls -l /tmp:\n{output}")
# Run a command with arguments
version_info = sh.python('-V')
print(f"\nPython version:\n{version_info}")
# Handle a non-zero exit code
try:
sh.false()
except sh.ErrorReturnCode_1 as e:
print(f"\nCaught expected error: {e.full_cmd}, Exit Code: {e.exit_code}")
# Pipe commands (similar to shell)
piped_output = sh.wc('-l', _in=sh.ls('-1'))
print(f"\nNumber of files in current directory: {piped_output.strip()}")