Drill 05: Subprocess & System

subprocess.run, environment variables, and system command patterns.

Run This Drill

bash ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/examples/python-drills/05-subprocess.sh

Drill Script

#!/bin/bash
# PYTHON DRILL 05: SUBPROCESS & SYSTEM
# Paste this entire script into your terminal
# Topics: subprocess, os, environment, command execution

echo "=================================================================="
echo "             PYTHON DRILL 05: SUBPROCESS & SYSTEM                "
echo "=================================================================="
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.1: SUBPROCESS.RUN (BASIC)"
echo "Execute shell commands"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import subprocess

# Simple command
result = subprocess.run(["echo", "Hello from subprocess"])
print(f"Return code: {result.returncode}")

# Capture output
result = subprocess.run(
    ["hostname"],
    capture_output=True,
    text=True
)
print(f"\nHostname: {result.stdout.strip()}")

# Command with arguments
result = subprocess.run(
    ["ls", "-la", "/tmp"],
    capture_output=True,
    text=True
)
print(f"\nFirst 3 lines of ls -la /tmp:")
for line in result.stdout.split("\n")[:3]:
    print(f"  {line}")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.2: SHELL=TRUE VS LIST"
echo "When to use shell mode"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import subprocess

# List form (preferred - safer)
result = subprocess.run(
    ["grep", "-c", "root", "/etc/passwd"],
    capture_output=True,
    text=True
)
print(f"grep result: {result.stdout.strip()}")

# Shell form (needed for pipes, redirects)
result = subprocess.run(
    "cat /etc/passwd | head -3",
    shell=True,
    capture_output=True,
    text=True
)
print(f"\nWith shell pipe:")
print(result.stdout)

# Shell for complex commands
result = subprocess.run(
    "ls /tmp/*.txt 2>/dev/null | wc -l",
    shell=True,
    capture_output=True,
    text=True
)
print(f".txt files in /tmp: {result.stdout.strip()}")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.3: ERROR HANDLING"
echo "Check return codes and stderr"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import subprocess

# Check for errors
result = subprocess.run(
    ["ls", "/nonexistent"],
    capture_output=True,
    text=True
)
print(f"Return code: {result.returncode}")
print(f"Stderr: {result.stderr.strip()}")

# Raise exception on failure
try:
    subprocess.run(
        ["ls", "/nonexistent"],
        capture_output=True,
        text=True,
        check=True  # Raises CalledProcessError on non-zero
    )
except subprocess.CalledProcessError as e:
    print(f"\nCaught error: {e}")
    print(f"Stderr: {e.stderr}")

# Safe pattern
def run_cmd(cmd):
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True
    )
    if result.returncode != 0:
        return None, result.stderr
    return result.stdout.strip(), None

output, error = run_cmd(["whoami"])
print(f"\nwhoami: {output}")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.4: ENVIRONMENT VARIABLES"
echo "Read and set env vars"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import os

# Read env var
user = os.environ.get("USER", "unknown")
home = os.environ.get("HOME", "/tmp")
print(f"USER: {user}")
print(f"HOME: {home}")

# Get with default
api_key = os.environ.get("API_KEY", "not-set")
print(f"API_KEY: {api_key}")

# Set env var for subprocess
import subprocess

# Pass modified environment
my_env = os.environ.copy()
my_env["MY_VAR"] = "hello"

result = subprocess.run(
    ["bash", "-c", "echo $MY_VAR"],
    capture_output=True,
    text=True,
    env=my_env
)
print(f"\nMY_VAR in subprocess: {result.stdout.strip()}")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.5: PRACTICAL PATTERNS"
echo "Real infrastructure scripts"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import subprocess
import json

# Get JSON output from command
def run_json(cmd):
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        check=True
    )
    return json.loads(result.stdout)

# Simulated - would be: kubectl get pods -o json
# For demo, use echo
result = subprocess.run(
    ["echo", '{"items": [{"name": "pod1"}, {"name": "pod2"}]}'],
    capture_output=True,
    text=True
)
data = json.loads(result.stdout)
print(f"Pods: {[p['name'] for p in data['items']]}")

# Run multiple commands
commands = [
    ["hostname"],
    ["date", "+%Y-%m-%d"],
    ["whoami"]
]

print("\n=== System info ===")
for cmd in commands:
    result = subprocess.run(cmd, capture_output=True, text=True)
    print(f"  {cmd[0]}: {result.stdout.strip()}")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 5.6: TIMEOUT AND ASYNC"
echo "Handle long-running commands"
echo "------------------------------------------------------------------"
echo ""
python3 << 'PYEOF'
import subprocess

# Timeout
try:
    result = subprocess.run(
        ["sleep", "10"],
        timeout=2,
        capture_output=True
    )
except subprocess.TimeoutExpired:
    print("Command timed out after 2 seconds")

# Background process (don't wait)
proc = subprocess.Popen(
    ["sleep", "1"],
    stdout=subprocess.PIPE
)
print(f"Started process: PID {proc.pid}")
print(f"Still running: {proc.poll() is None}")

# Wait with timeout
try:
    proc.wait(timeout=2)
    print(f"Process finished: {proc.returncode}")
except subprocess.TimeoutExpired:
    proc.kill()
    print("Killed long-running process")
PYEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "YOUR TURN - TRY THESE:"
echo "------------------------------------------------------------------"
echo ""
echo "1. Get disk usage:"
echo "   import subprocess"
echo "   result = subprocess.run(['df', '-h', '/'], capture_output=True, text=True)"
echo "   print(result.stdout)"
echo ""
echo "2. Check if host is reachable:"
echo "   result = subprocess.run(['ping', '-c', '1', '-W', '1', '8.8.8.8'],"
echo "                           capture_output=True)"
echo "   print('Reachable' if result.returncode == 0 else 'Unreachable')"
echo ""
echo "3. Get env var with fallback:"
echo "   import os"
echo "   vault_addr = os.environ.get('VAULT_ADDR', 'https://127.0.0.1:8200')"
echo ""
echo "------------------------------------------------------------------"
echo "KEY TAKEAWAYS:"
echo "1. subprocess.run([cmd, args], capture_output=True, text=True)"
echo "2. Use list form for safety, shell=True for pipes"
echo "3. check=True raises exception on failure"
echo "4. os.environ.get('VAR', 'default') for env vars"
echo "5. timeout= prevents hangs"
echo "------------------------------------------------------------------"