What is the Sh library
In many Python projects there is a need to invoke shell commands: manage files, run system processes, interact with installed CLI tools. You can use os.system or subprocess, but they are bulky and require manual handling of arguments, errors, and output.
The Sh library provides a clean, pythonic way to call shell commands as regular Python functions. It greatly simplifies integration of external utilities, makes code readable and easy to debug.
Advantages of using Sh
The Sh library offers several important benefits over standard methods of executing system commands:
Readability and simple syntax – commands look like ordinary Python functions, which makes the code more understandable and maintainable.
Automatic error handling – the library automatically processes return codes and raises exceptions on failures.
Security – protection against command injection thanks to proper argument escaping.
Flexibility – support for pipelines, background execution, streaming I/O and many other features.
Installation and import
Install the library via pip:
pip install sh
Importing it in code requires just one line:
import sh
Core concepts
Sh automatically maps the name of any shell command to a Python function. This allows you to call them naturally:
print(sh.ls("-l"))
Each call returns an object containing stdout, which can be converted to a string, a list of strings, bytes, and other formats.
Basic usage
File operations
# List files
print(sh.ls("-lah"))
# Create a file
sh.touch("file.txt")
# Copy a file
sh.cp("file.txt", "copy.txt")
# Delete a file
sh.rm("copy.txt")
# Create a directory
sh.mkdir("test_dir")
# Move a file
sh.mv("file.txt", "test_dir/")
Text operations
# Show file content
content = sh.cat("file.txt")
print(str(content))
# Search in files
matches = sh.grep("python", "*.py")
for match in matches:
print(match.strip())
# Count lines
line_count = sh.wc("-l", "file.txt")
print(f"Line count: {line_count.strip()}")
Working with arguments and options
Sh supports various ways to pass command arguments:
Positional arguments
sh.ls("--color", "auto", "/home/user")
Keyword arguments
# Long options
sh.ls(color="auto", all=True, human_readable=True)
# Short options
sh.ls(a=True, l=True, h=True)
Combined approach
# Mixed usage
sh.find("/home", name="*.py", type="f")
Processing command output
Getting the result as a string
output = sh.ls()
print(str(output))
Line‑by‑line processing
for line in output:
print(line.strip())
Working with bytes
raw_output = sh.ls()
bytes_data = raw_output.stdout
Splitting into a list of strings
lines = str(sh.ls()).split('\n')
for line in lines:
if line.strip():
print(line)
Error handling
The Sh library provides powerful mechanisms for dealing with errors:
Basic error handling
try:
sh.rm("non_existent.txt")
except sh.ErrorReturnCode_1 as e:
print("Error:", e.stderr.decode())
Handling different exit codes
try:
result = sh.command_that_might_fail()
except sh.ErrorReturnCode as e:
print(f"Command exited with code: {e.exit_code}")
print(f"Stderr: {e.stderr.decode()}")
print(f"Stdout: {e.stdout.decode()}")
Ignoring specific error codes
# Allow exit codes 0 and 1
sh.grep("pattern", "file.txt", _ok_code=[0, 1])
Integration with pipelines
Sh supports powerful Unix‑style pipeline capabilities:
Simple pipeline
result = sh.grep("python", _in=sh.ls("-l"))
print(result)
Command chain
# Equivalent to: ls -la | grep ".py" | wc -l
count = sh.wc("-l", _in=sh.grep(".py", _in=sh.ls("-la")))
print(f"Number of Python files: {count.strip()}")
Using the pipe operator
# Alternative syntax
result = (sh.ls("-la") | sh.grep(".py") | sh.wc("-l"))
print(result)
Streaming input and output
Sending data to a command
# Send a string
sh.cat(_in="Hello from Python\n")
# Send a file
with open("input.txt", "r") as f:
sh.sort(_in=f)
Real‑time reading
# Tail a log file in real time
proc = sh.tail("-f", "log.txt", _iter=True)
for line in proc:
print(line.strip())
# Add processing logic here
Redirecting output
# Write to a file
sh.ls("-la", _out="output.txt")
# Append to a file
sh.echo("New line", _out="output.txt", _append=True)
Working with custom commands
Creating a command from a path
my_tool = sh.Command("/usr/local/bin/mycli")
result = my_tool("--run", path="/tmp")
Checking command availability
if sh.which("docker"):
print("Docker is installed")
docker_version = sh.docker("--version")
print(docker_version)
else:
print("Docker not found")
Parallel execution
Background execution
# Run in background
p = sh.sleep(10, _bg=True)
print("Running...")
# Do other work
p.wait() # Wait for completion
Process management
# Get PID
proc = sh.long_running_command(_bg=True)
print(f"Process PID: {proc.pid}")
# Check status
if proc.is_alive():
print("Process is still running")
# Force termination
proc.kill()
Working with the environment
Changing the working directory
with sh.pushd("/tmp"):
sh.touch("test.txt")
files = sh.ls()
print(files)
# Automatically returns to the previous directory
Managing environment variables
with sh.env(MY_VAR="123", DEBUG="true"):
result = sh.printenv("MY_VAR")
print(result)
Running from a different directory
# Execute a command from a specific cwd
sh.ls(_cwd="/home/user/projects")
Advanced features
Command timeout
try:
sh.sleep(10, _timeout=2)
except sh.TimeoutException:
print("Command exceeded time limit")
Working with TTY
# For commands that require a terminal
sh.sudo("apt", "update", _tty_in=True)
Automatic output decoding
# Decode to a string automatically
result = sh.ls(_decode=True)
print(type(result)) #
Table of main methods and parameters
| Method/Parameter | Description | Example usage |
|---|---|---|
sh.<command>() |
Invoke a system command | sh.ls("-la") |
sh.Command(path) |
Create a command from a file path | sh.Command("/bin/custom") |
sh.which(cmd) |
Find a command in PATH | sh.which("python3") |
sh.pushd(path) |
Change directory (context manager) | with sh.pushd("/tmp"): |
sh.env(**vars) |
Modify environment variables | with sh.env(VAR="val"): |
_in |
Pass input data | sh.cat(_in="text") |
_out |
Redirect output | sh.ls(_out="file.txt") |
_err |
Redirect error stream | sh.cmd(_err="errors.txt") |
_bg |
Background execution | sh.sleep(10, _bg=True) |
_timeout |
Execution time limit | sh.cmd(_timeout=30) |
_cwd |
Working directory | sh.ls(_cwd="/home") |
_ok_code |
Allowed exit codes | sh.grep("pat", _ok_code=[0,1]) |
_tty_in |
Use TTY for input | sh.sudo("cmd", _tty_in=True) |
_tty_out |
Use TTY for output | sh.cmd(_tty_out=True) |
_iter |
Iterate over lines | for line in sh.ls(_iter=True): |
_decode |
Auto‑decode output | sh.ls(_decode=True) |
_err_to_out |
Merge stderr into stdout | sh.cmd(_err_to_out=True) |
_append |
Append to a file | sh.echo("text", _out="f", _append=True) |
_long_opts |
Long options as a dict | sh.cmd(_long_opts={"--opt": "val"}) |
.stdout |
Access stdout | result.stdout |
.stderr |
Access stderr | result.stderr |
.exit_code |
Exit status code | result.exit_code |
.pid |
Process ID | proc.pid |
.wait() |
Wait for completion | proc.wait() |
.kill() |
Force termination | proc.kill() |
.is_alive() |
Check if running | proc.is_alive() |
Security and compatibility
Supported platforms
Linux – full feature support
macOS – full Unix‑command support
Windows – limited support via WSL or CMD
Security benefits
The Sh library provides a higher security level compared with alternatives:
- Automatic argument escaping prevents injection attacks
- Safe parameter passing without risk of arbitrary code execution
- Process isolation and lifecycle control
Comparison with other methods
| Method | API level | Security | Readability | Error handling | Flexibility |
|---|---|---|---|---|---|
os.system |
Low | Low | Low | No | Low |
subprocess |
Medium | Medium | Medium | Yes | Medium |
sh |
High | High | Excellent | Yes | High |
Practical usage examples
DevOps automation
import sh
def deploy_application():
# Update code
sh.git("pull", "origin", "main")
# Install dependencies
sh.pip("install", "-r", "requirements.txt")
# Run tests
try:
sh.pytest("tests/")
except sh.ErrorReturnCode as e:
print(f"Tests failed: {e.stderr.decode()}")
return False
# Restart service
sh.systemctl("restart", "myapp")
return True
System monitoring
def system_health_check():
# Disk usage
disk_usage = sh.df("-h")
print("Disk usage:")
print(disk_usage)
# Memory usage
memory = sh.free("-h")
print("Memory usage:")
print(memory)
# CPU load
cpu_load = sh.uptime()
print("System load:")
print(cpu_load)
Git operations
def git_operations():
# Repository status
try:
status = sh.git("status", "--porcelain")
if status.strip():
print("Uncommitted changes present")
else:
print("Working directory clean")
except sh.ErrorReturnCode:
print("Not a Git repository")
# Recent commits
commits = sh.git("log", "--oneline", "-5")
print("Last 5 commits:")
print(commits)
Handling common errors
Command not found
try:
sh.nonexistent_command()
except sh.CommandNotFound as e:
print(f"Command not found: {e}")
Invalid arguments
try:
sh.ls("--invalid-option")
except sh.ErrorReturnCode as e:
print(f"Invalid option: {e.stderr.decode()}")
Timeout exceeded
try:
sh.long_command(_timeout=10)
except sh.TimeoutException:
print("Command exceeded time limit")
Best practices
Error handling
Always catch exceptions when invoking commands that may fail:
try:
result = sh.risky_command()
except sh.ErrorReturnCode as e:
logger.error(f"Command failed: {e}")
# Additional error processing
Using context managers
For operations that require environment changes, use context managers:
with sh.pushd("/project"):
with sh.env(ENV="production"):
sh.make("deploy")
Command validation
Check for command availability before use:
if not sh.which("docker"):
raise RuntimeError("Docker is not installed")
Frequently asked questions
How to get only the command’s exit code?
try:
sh.test_command()
exit_code = 0
except sh.ErrorReturnCode as e:
exit_code = e.exit_code
How to run a command with sudo?
sh.sudo("systemctl", "restart", "nginx", _tty_in=True)
How to pass multi‑line text to a command?
text = """
Line 1
Line 2
Line 3
"""
sh.cat(_in=text)
How to get command output as a list of lines?
lines = str(sh.ls("-1")).strip().split('\n')
How to run a command with environment variables?
with sh.env(MY_VAR="value"):
sh.command_that_uses_env()
Can sh be used in multithreaded applications?
Yes, but with care. Each command spawns a separate process, which is safe for parallel execution.
Limitations and alternatives
Limitations
- Works only on Unix‑like systems (Linux, macOS)
- Limited Windows support
- Not suitable for commands requiring interactive input
- May be slower than direct
subprocesscalls for very simple cases
Alternatives
If Sh doesn’t fit your needs, consider:
- subprocess – lower‑level control
- plumbum – similar API, alternative library
- fabric – remote command execution
- invoke – building CLI applications
Conclusion
Sh is a high‑level library for invoking system commands in Python, offering concise and readable syntax. It is ideal for automation scripts, DevOps tooling, and integrating CLI interfaces into Python applications.
Key advantages of the library:
- Intuitive Pythonic API
- Powerful error‑handling capabilities
- Support for pipelines and streaming I/O
- Secure command execution
- Highly configurable execution options
With these features and minimal syntax, Sh makes working with system commands truly pythonic and efficient.
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed