cmd-proc 0.0.1

Process command builder with capture support
Documentation

cmd-proc - Process Command Builder

A wrapper around std::process::Command providing debug logging, stronger input types, and a fluent builder API with automatic exit code checking.

Note: This crate primarily exists to serve the mbj/mrs monorepo crates. While pre-1.0, the API is subject to change without notice. External use is welcome but not the primary goal.

Why cmd-proc?

std::process::Command is powerful but requires verbose boilerplate for common patterns:

// std::process - capturing stdout as string
let output = std::process::Command::new("git")
    .args(["rev-parse", "HEAD"])
    .output()?;
if !output.status.success() {
    return Err(/* construct error with exit status */);
}
let stdout = String::from_utf8(output.stdout)?;

// cmd-proc - same operation
let stdout = cmd_proc::Command::new("git")
    .arguments(["rev-parse", "HEAD"])
    .stdout()
    .string()?;

Key differences from std::process

Feature std::process cmd-proc
Debug logging None Automatic debug logging of commands before execution
Exit code checking Manual Automatic - non-zero exits return Err
Output capture Returns raw Output struct Two-step pattern: .stdout().string(), .stderr().bytes()
Builder pattern Mutable references Owned builder with method chaining
Stdin data Requires manual pipe setup Simple .stdin_bytes() method
Env var names Accepts any &str EnvVariableName type with compile-time validation
Error type Separate io::Error and ExitStatus Unified CommandError with both
Process spawning Direct .spawn() on Command Separate Spawn builder with Stdio configuration

Design philosophy

  • Debug logging: Every command execution is logged via the log crate at debug level, making it easy to trace what commands are being run.
  • Stronger input types: EnvVariableName prevents invalid environment variable names (empty or containing =) at compile time rather than runtime.
  • Two-step capture pattern: The .stdout() and .stderr() methods return a Capture builder, which provides .bytes() and .string() methods. This separates stream selection from output format.
  • Mandatory exit code checking: Capture methods always treat non-zero exit as an error, preventing accidentally ignored failures.
  • Fluent API: Chain configuration methods naturally without &mut self gymnastics.

Usage

use cmd_proc::{Command, Capture, Output, EnvVariableName};

// Capture stdout as string (two-step pattern)
let sha = Command::new("git")
    .arguments(["rev-parse", "HEAD"])
    .stdout()
    .string()?;

// Capture stderr as bytes
let errors = Command::new("cargo")
    .argument("build")
    .stderr()
    .bytes()?;

// Run without capturing (just check success)
Command::new("cargo")
    .arguments(["fmt", "--check"])
    .status()?;

// Pass stdin data
let output = Command::new("cat")
    .stdin_bytes(b"hello world")
    .stdout()
    .string()?;

// Set environment variables (compile-time validated)
const MY_VAR: EnvVariableName = EnvVariableName::from_static("MY_VAR");
Command::new("sh")
    .arguments(["-c", "echo $MY_VAR"])
    .env(&MY_VAR, "value")
    .status()?;

// Set working directory
Command::new("cargo")
    .argument("build")
    .working_directory("/path/to/project")
    .status()?;

The Capture Pattern

Output capture uses a two-step pattern via the Capture struct:

Command::new("git")
    .argument("status")
    .stdout()      // Returns Capture (selects which stream)
    .string()?;    // Executes and returns output in chosen format

The Capture struct is returned by .stdout() or .stderr() and provides:

  • .bytes() - Execute and return output as Vec<u8>
  • .string() - Execute and return output as String (with UTF-8 validation)

This separation makes the API explicit about which stream is being captured and in what format.

Full Output Access

When you need both streams or want to handle failures with stderr access, use .output():

let output = Command::new("git")
    .arguments(["show", "some-ref:path"])
    .output()?;

if output.success() {
    let content = output.into_stdout_string()?;
    // use content
} else {
    let error_message = output.into_stderr_string()?;
    // handle error with stderr
}

The Output struct provides:

  • stdout: Vec<u8> - Raw stdout bytes
  • stderr: Vec<u8> - Raw stderr bytes
  • status: ExitStatus - Exit status
  • .success() - Check if command succeeded
  • .into_stdout_string() - Convert stdout to String (strict UTF-8, consumes self)
  • .into_stderr_string() - Convert stderr to String (strict UTF-8, consumes self)

Unlike Capture, .output() does not treat non-zero exit as an error - it only fails on IO errors.

Spawning Long-Running Processes

For processes that need interactive stdin/stdout or run in the background, use .spawn():

use cmd_proc::{Command, Stdio};
use std::io::BufRead;

let mut child = Command::new("my-server")
    .argument("--port=8080")
    .spawn()
    .stdin(Stdio::Piped)
    .stdout(Stdio::Piped)
    .stderr(Stdio::Inherit)
    .run()?;

// Read from stdout
let line = std::io::BufReader::new(child.stdout().unwrap())
    .lines()
    .next()
    .unwrap()?;

// Close stdin to signal shutdown
drop(child.take_stdin());

// Wait for exit
let status = child.wait()?;

The Stdio enum controls stream handling:

  • Stdio::Piped - Capture the stream for reading/writing
  • Stdio::Inherit - Pass through to parent process (default)
  • Stdio::Null - Redirect to /dev/null

The Child struct provides:

  • .stdin(), .stdout(), .stderr() - Mutable references to handles
  • .take_stdin(), .take_stdout(), .take_stderr() - Take ownership of handles
  • .wait() - Wait for exit, returns ExitStatus
  • .wait_with_output() - Wait and collect output as Output

Environment Variables

Environment variable names are validated via EnvVariableName:

  • Cannot be empty - an empty name is meaningless
  • Cannot contain = - the OS uses = as separator between name and value; a name containing = corrupts the environment block

std::process::Command::env() silently accepts invalid names, causing mysterious runtime failures. EnvVariableName catches errors at compile time (from_static) or parse time (FromStr).

use cmd_proc::{Command, EnvVariableName};

// Compile-time validated (panics at compile time if invalid)
const MY_VAR: EnvVariableName = EnvVariableName::from_static("MY_VAR");
Command::new("sh")
    .env(&MY_VAR, "value")
    .status()?;

// Set multiple variables from an iterator
let vars = [("FOO", "1"), ("BAR", "2")];
Command::new("sh")
    .envs(vars)
    .status()?;

// Remove a variable from the child environment
const PATH: EnvVariableName = EnvVariableName::from_static("PATH");
Command::new("sh")
    .env_remove(&PATH)
    .status()?;

Error Handling

CommandError unifies IO errors (command not found, permission denied) and non-zero exit status:

match Command::new("might-fail").status() {
    Ok(()) => println!("Success"),
    Err(e) => {
        if let Some(io_error) = &e.io_error {
            eprintln!("IO error: {io_error}");
        }
        if let Some(exit_status) = &e.exit_status {
            eprintln!("Exit code: {:?}", exit_status.code());
        }
    }
}

License

See workspace root for license information.