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.
Status: Pre-1.0 - exists to serve mbj/mrs monorepo, expect breaking changes without notice.
Why cmd-proc?
std::process::Command is powerful but requires verbose boilerplate for common patterns:
// std::process - capturing stdout as string
let output = new
.args
.output?;
if !output.status.success
let stdout = Stringfrom_utf8?;
// cmd-proc - same operation
let stdout = new
.arguments
.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
logcrate at debug level, making it easy to trace what commands are being run. - Stronger input types:
EnvVariableNameprevents invalid environment variable names (empty or containing=) at compile time rather than runtime. - Two-step capture pattern: The
.stdout()and.stderr()methods return aCapturebuilder, 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 selfgymnastics.
Usage
use ;
// Capture stdout as string (two-step pattern)
let sha = new
.arguments
.stdout
.string?;
// Capture stderr as bytes
let errors = new
.argument
.stderr
.bytes?;
// Run without capturing (just check success)
new
.arguments
.status?;
// Pass stdin data
let output = new
.stdin_bytes
.stdout
.string?;
// Set environment variables (compile-time validated)
const MY_VAR: EnvVariableName = from_static_or_panic;
new
.arguments
.env
.status?;
// Set working directory
new
.argument
.working_directory
.status?;
Arguments vs Options
In CLI terminology:
- Argument: positional value (e.g.,
git clone <url>) - Option: named parameter with value (e.g.,
--message "text",-o file)
cmd-proc provides methods for both:
// Arguments (positional)
new
.argument // single argument
.argument // another argument
.arguments // multiple arguments
.optional_argument // argument only if Some
.status?;
// Options (name + value pairs)
new
.argument
.option // required option
.optional_option // option only if Some
.status?;
The option and optional_option methods simplify the common pattern of adding a flag followed by its value:
// Instead of:
if let Some = maybe_author
// Use:
command.optional_option
The Capture Pattern
Output capture uses a two-step pattern via the Capture struct:
new
.argument
.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 asVec<u8>.string()- Execute and return output asString(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 = new
.arguments
.output?;
if output.success else
The Output struct provides:
stdout: Vec<u8>- Raw stdout bytesstderr: Vec<u8>- Raw stderr bytesstatus: 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 ;
use BufRead;
let mut child = new
.argument
.spawn
.stdin
.stdout
.stderr
.run?;
// Read from stdout
let line = new
.lines
.next
.unwrap?;
// Close stdin to signal shutdown
drop;
// Wait for exit
let status = child.wait?;
The Stdio enum controls stream handling:
Stdio::Piped- Capture the stream for reading/writingStdio::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, returnsExitStatus.wait_with_output()- Wait and collect output asOutput
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 (when from_static_or_panic is used in a const context) or parse time (FromStr).
use ;
// Compile-time validated (panics at compile time if invalid)
const MY_VAR: EnvVariableName = from_static_or_panic;
new
.env
.status?;
// Set multiple variables from an iterator
let vars = ;
new
.envs
.status?;
// Remove a variable from the child environment
const PATH: EnvVariableName = from_static_or_panic;
new
.env_remove
.status?;
Error Handling
CommandError unifies IO errors (command not found, permission denied) and non-zero exit status:
match new.status