Run Commands With Argument Arrays Not Shell Strings
A command runner that executes binaries with an argument array and no shell, so untrusted values can never be interpreted as shell syntax.
Code
import subprocess
from dataclasses import dataclass
from typing import Mapping, Optional, Sequence
class UnsafeCommandError(ValueError):
"""Raised when a command would require shell interpretation."""
@dataclass(frozen=True)
class CommandResult:
argv: Sequence[str]
returncode: int
stdout: str
stderr: str
def run_command(
binary: str,
args: Sequence[str],
*,
timeout_s: float = 10.0,
env: Optional[Mapping[str, str]] = None,
use_shell: bool = False,
) -> CommandResult:
"""Run binary with an argv list and no shell; return a structured result."""
if use_shell:
raise UnsafeCommandError("shell mode is refused; pass an argv list instead")
if not binary:
raise ValueError("binary must be a non-empty path")
argv = [binary, *[str(a) for a in args]]
try:
completed = subprocess.run(
argv,
shell=False,
capture_output=True,
text=True,
timeout=timeout_s,
env=dict(env) if env is not None else None,
)
except subprocess.TimeoutExpired as exc:
raise TimeoutError("command timed out after %ss" % timeout_s) from exc
except FileNotFoundError as exc:
raise FileNotFoundError("binary not found: %s" % binary) from exc
return CommandResult(
argv=argv,
returncode=completed.returncode,
stdout=completed.stdout,
stderr=completed.stderr,
)Report a problem with this exampleOpens a pre-filled issue on GitHub.