Before:
result = subprocess.run(["git", "status"], capture_output=True, text=True, check=True)
print(result.stdout)
After:
from zap import run
print(run("git status"))
Install
pip install zap-sh
Quick Start
from zap import run
# Run a command — returns a Result object
result = run("git status")
print(result.stdout) # raw stdout
print(result.ok) # True if exit code 0
print(result.code) # exit code
print(result.lines) # stdout as list of lines
# Just print it — str() returns stdout
print(run("ls -la"))
Pipe Chaining
# Pipe with strings — clean and simple
result = run("cat logs.txt") | "grep ERROR" | "wc -l"
print(result) # number of error lines
Live Output
See output in real-time while still capturing it. No more staring at a blank screen during pip install or docker build.
# Stream output live to terminal — still captured in result
result = run("pip install flask", live=True)
# Works with long builds too
run("docker build -t myapp .", live=True)
run("make all", live=True)
Retry
Automatically retry flaky commands — network calls, API requests, CI scripts.
# Retry up to 3 times with 2s delay between attempts
result = run("curl -f https://api.example.com/health", retries=3, delay=2)
# Only retries on failure (non-zero exit) — succeeds immediately if ok
run("flaky-deploy-script.sh", retries=5, delay=5)
Check If a Command Exists
from zap import which
# Returns the path if found, None if not
if which("docker"):
run("docker ps")
else:
print("Docker not installed")
# Great for setup scripts
for cmd in ["git", "node", "python3"]:
path = which(cmd)
print(f"{cmd}: {path or 'NOT FOUND'}")
Temporary Directory
from zap import cd
# Change directory temporarily — auto-restores when done
with cd("/my/project"):
run("git status")
run("npm install")
run("npm run build")
# Back to original directory here
# Safe even if something errors
with cd("/tmp"):
run("some-command") # if this fails, directory still restores
Error Handling
from zap import run, ZapError
# Raises ZapError on non-zero exit (default)
try:
run("git push origin fake-branch")
except ZapError as e:
print(e.stderr)
print(e.code)
# Disable with check=False
result = run("might-fail", check=False)
if not result.ok:
print("Failed:", result.stderr)
Options
# Timeout (seconds)
run("sleep 100", timeout=5) # raises TimeoutError
# Working directory
run("ls", cwd="/tmp")
# Environment variables (merged with os.environ)
run("echo $API_KEY", env={"API_KEY": "secret"})
# Feed stdin
run("cat", stdin="hello from stdin")
# Safe mode — pass a list instead of string
run(["echo", "no shell injection"])
Async
from zap import run_async
result = await run_async("git status")
print(result.ok)
Same API as run() — just await it.
Result Object
| Property | Type | Description |
|---|---|---|
.stdout |
str |
Captured stdout |
.stderr |
str |
Captured stderr |
.code |
int |
Exit code |
.ok |
bool |
True if exit code is 0 |
.lines |
list[str] |
Stdout split into lines |
.json() |
Any |
Parse stdout as JSON |
str(r) |
str |
Returns stdout stripped |
bool(r) |
bool |
Returns .ok |
r | "cmd" |
Result |
Pipe stdout to next command |
run() Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
command |
str | list |
required | Command to run |
check |
bool |
True |
Raise ZapError on non-zero exit |
timeout |
float |
None |
Timeout in seconds |
cwd |
str |
None |
Working directory |
env |
dict |
None |
Extra env vars (merged with os.environ) |
stdin |
str |
None |
String to feed as stdin |
live |
bool |
False |
Stream output in real-time |
retries |
int |
0 |
Number of retry attempts on failure |
delay |
float |
1.0 |
Seconds between retries |
How It Works
- String command → runs with
shell=True(convenience for scripting) - List command → runs with
shell=False(safe, no shell injection) - check=True by default → raises
ZapErroron non-zero exit - live=True → uses
Popenwithselectorsfor real-time streaming - retries → catches failures and retries with configurable delay
- Zero dependencies → pure Python stdlib
- ~300 lines → read the whole source in 5 minutes
Support
If Zap saves you time, consider buying me a coffee.
License
MIT
Metadata
Release files for zap-sh 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zap_sh-0.2.0.tar.gz | 10.8 kB | Without provenance | Details for zap_sh-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zap_sh-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for zap_sh-0.2.0-py3-none-any.whl |
Total release size: 18.4 kB
Release files / zap_sh-0.2.0.tar.gz
| Download URL | zap_sh-0.2.0.tar.gz |
|---|---|
| Size | 10.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
81644762b4a99d401448cb619628a6a05ad0ef9fab7186ca5ea97f3a147e7a33
|
|
BLAKE2b-256 checksum How to use checksums |
8bed8e05040859c7a9ccfc3b53b4c013c50605bac53511a577a793f24e7c1dbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/5.1.1 CPython/3.12.7
|
Release files / zap_sh-0.2.0-py3-none-any.whl
| Download URL | zap_sh-0.2.0-py3-none-any.whl |
|---|---|
| Size | 7.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
23b0bbc6de7676f8515c1943334dc3366adcccde063a30709ee77589c08410b1
|
|
BLAKE2b-256 checksum How to use checksums |
c899ecb0b2f6849ef5df3c82f6986e489382f364da387e54c4b797eadae276be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/5.1.1 CPython/3.12.7
|
Security for zap-sh 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.