A more ergonomic subprocess alternative with bash-like pipe syntax
Project description
shell-pilot
A more ergonomic subprocess alternative for Python with bash-like pipe syntax.
Why?
Python's subprocess module is powerful but verbose. Compare:
# subprocess (the pain)
import subprocess
p1 = subprocess.Popen(['ls', '-la'], stdout=subprocess.PIPE)
p2 = subprocess.Popen(['grep', 'foo'], stdin=p1.stdout, stdout=subprocess.PIPE)
p3 = subprocess.Popen(['wc', '-l'], stdin=p2.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
p2.stdout.close()
output = p3.communicate()[0]
# shell-pilot (the joy)
from shell_pilot import cmd
result = (cmd("ls -la") | cmd("grep foo") | cmd("wc -l")).run()
Installation
pip install shell-pilot
# Note: Import uses underscore (Python convention)
from shell_pilot import cmd
Usage
Basic Commands
from shell_pilot import cmd
# Simple command
result = cmd("echo hello world").run()
print(result.stdout) # "hello world\n"
# Check success
if result.ok:
print("Command succeeded")
Piping with |
Chain commands together just like in bash:
# Two-stage pipe
result = (cmd("cat /etc/hosts") | cmd("grep localhost")).run()
# Multi-stage pipe
result = (
cmd("ps aux")
| cmd("grep python")
| cmd("grep -v grep")
| cmd("wc -l")
).run()
print(f"Python processes: {result.stdout.strip()}")
Stdin, Environment & Working Directory
# Pass stdin
result = cmd("cat").with_stdin("hello from stdin").run()
# Set environment variables
result = cmd("echo $GREETING").with_env(GREETING="Hello").run()
# Change working directory
result = cmd("ls").with_cwd("/tmp").run()
Fluent API
All configuration methods return new instances, allowing fluent chaining:
result = (
cmd("my-script")
.with_stdin(input_data)
.with_env(DEBUG="1", LOG_LEVEL="info")
.with_cwd("/app")
.run(check=True)
)
Timeouts
Prevent commands from hanging indefinitely:
from shell_pilot import cmd, TimeoutExpired
try:
result = cmd("long-running-command").run(timeout=30.0)
except TimeoutExpired as e:
print(f"Command timed out after {e.timeout}s")
# Also works with pipelines and async
result = (cmd("producer") | cmd("consumer")).run(timeout=10.0)
result = await cmd("async-command").run_async(timeout=5.0)
Shell Mode
Use shell features like glob expansion and environment variable substitution:
# Glob expansion
result = cmd("ls *.py", shell=True).run()
# Environment variable expansion
result = cmd("echo $HOME", shell=True).run()
# Shell operators
result = cmd("cmd1 && cmd2 || cmd3", shell=True).run()
Security Warning: Using
shell=Truewith untrusted input creates command injection vulnerabilities. Never pass user-provided strings directly to shell mode. For pipelines, prefer the|operator which is both safer and more Pythonic.
Streaming
Process output incrementally without loading everything into memory:
# Line-by-line streaming
with cmd("tail -f /var/log/syslog").stream() as s:
for line in s.iter_lines():
print(f"Got: {line}")
if "error" in line:
break # Process terminates automatically
# Byte-level streaming
with cmd("cat /dev/urandom").stream() as s:
for chunk in s.iter_bytes(chunk_size=4096):
process_binary(chunk)
if done:
break
# Pipeline streaming
with (cmd("producer") | cmd("filter")).stream() as s:
for line in s.iter_lines():
handle(line)
# Async streaming
async with await cmd("async-producer").stream_async() as s:
async for line in s.iter_lines():
await handle(line)
Error Handling
from shell_pilot import cmd, CommandError
# Option 1: Check the result
result = cmd("might-fail").run()
if not result.ok:
print(f"Failed with code {result.returncode}: {result.stderr}")
# Option 2: Raise on failure
try:
result = cmd("must-succeed").run(check=True)
except CommandError as e:
print(f"Command failed: {e.result.stderr}")
# Option 3: Chain raise_on_error()
result = cmd("risky-command").run().raise_on_error()
Async Support
import asyncio
from shell_pilot import cmd
async def main():
# Single async command
result = await cmd("echo async").run_async()
# Async pipeline (runs concurrently!)
result = await (
cmd("cat largefile.txt")
| cmd("grep pattern")
| cmd("sort -u")
).run_async()
# With timeout
result = await cmd("slow-command").run_async(timeout=10.0)
asyncio.run(main())
API Reference
cmd(command, stdin=None, env=None, cwd=None, shell=False)
Create a command. Strings are automatically parsed (e.g., "ls -la" becomes ["ls", "-la"]).
Methods:
.run(check=False, timeout=None)- Execute synchronously.run_async(check=False, timeout=None)- Execute asynchronously.stream()- Start command and return Stream for incremental reading.stream_async()- Start command and return AsyncStream.with_stdin(text)- Set stdin input.with_env(**vars)- Add environment variables.with_cwd(path)- Set working directory|- Pipe to another command
Result
Attributes:
.stdout- Standard output as string.stderr- Standard error as string.returncode- Exit code.ok-Trueif returncode is 0
Methods:
.raise_on_error()- RaiseCommandErrorif failed, returns self for chainingbool(result)- Returns.okstr(result)- Returns.stdout
Stream / AsyncStream
Properties:
.stdout- Raw stdout file object (for select/poll).stderr- Raw stderr file object.pid- PID of last process in pipeline.returncodes- List of return codes (None if still running)
Methods:
.iter_bytes(chunk_size=8192)- Iterate over byte chunks.iter_lines(encoding="utf-8")- Iterate over lines.read_all(timeout=None)- Read remaining output as Result.kill()/.terminate()- Signal all processes.close()- Clean up resources
Exceptions
CommandError- Raised when command fails (withcheck=True)TimeoutExpired- Raised when command exceeds timeout
Convenience Functions
from shell_pilot import run, run_async
result = run("ls -la") # Quick sync execution
result = await run_async("ls -la") # Quick async execution
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shell_pilot-0.2.0.tar.gz.
File metadata
- Download URL: shell_pilot-0.2.0.tar.gz
- Upload date:
- Size: 10.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59742cd8f7c9a910f603e934194ca10d35bce72ef562bb0889f21baae5d4e204
|
|
| MD5 |
28b77b70b8c735e793403c3f3b5ce9b8
|
|
| BLAKE2b-256 |
bf9df11ff812e4a4ac8aa40a179838e60bc13832d6d25415550c96132f37a557
|
File details
Details for the file shell_pilot-0.2.0-py3-none-any.whl.
File metadata
- Download URL: shell_pilot-0.2.0-py3-none-any.whl
- Upload date:
- Size: 10.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad5f562a3a4890b8dc632fb399abf4580cd6ec592d3cd6402e13f8b358f27e47
|
|
| MD5 |
a01af1d3b15b6350a87d15d6086b4166
|
|
| BLAKE2b-256 |
665f9bf93a53a4fe3cc18c0602487b93bf7181cf662af750670ec94e350a7522
|