Skip to main content

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=True with 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 - True if returncode is 0

Methods:

  • .raise_on_error() - Raise CommandError if failed, returns self for chaining
  • bool(result) - Returns .ok
  • str(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 (with check=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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shell_pilot-0.2.0.tar.gz (10.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shell_pilot-0.2.0-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

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

Hashes for shell_pilot-0.2.0.tar.gz
Algorithm Hash digest
SHA256 59742cd8f7c9a910f603e934194ca10d35bce72ef562bb0889f21baae5d4e204
MD5 28b77b70b8c735e793403c3f3b5ce9b8
BLAKE2b-256 bf9df11ff812e4a4ac8aa40a179838e60bc13832d6d25415550c96132f37a557

See more details on using hashes here.

File details

Details for the file shell_pilot-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for shell_pilot-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ad5f562a3a4890b8dc632fb399abf4580cd6ec592d3cd6402e13f8b358f27e47
MD5 a01af1d3b15b6350a87d15d6086b4166
BLAKE2b-256 665f9bf93a53a4fe3cc18c0602487b93bf7181cf662af750670ec94e350a7522

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page