| Crates.io | cmd-proc |
| lib.rs | cmd-proc |
| version | 0.1.0 |
| created_at | 2026-01-20 16:46:22.426724+00 |
| updated_at | 2026-01-21 13:46:34.094186+00 |
| description | Process command builder with capture support |
| homepage | |
| repository | https://github.com/mbj/mrs/tree/main/cmd-proc |
| max_upload_size | |
| id | 2057002 |
| size | 37,843 |
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.
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()?;
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 |
log crate at debug level, making it easy to trace what commands are being run.EnvVariableName prevents invalid environment variable names (empty or containing =) at compile time rather than runtime..stdout() and .stderr() methods return a Capture builder, which provides .bytes() and .string() methods. This separates stream selection from output format.&mut self gymnastics.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_or_panic("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()?;
In CLI terminology:
git clone <url>)--message "text", -o file)cmd-proc provides methods for both:
// Arguments (positional)
Command::new("git")
.argument("clone") // single argument
.argument(url) // another argument
.arguments(["--depth", "1"]) // multiple arguments
.optional_argument(maybe_path) // argument only if Some
.status()?;
// Options (name + value pairs)
Command::new("git")
.argument("commit")
.option("--message", "fix bug") // required option
.optional_option("--author", maybe_author) // 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(author) = maybe_author {
command = command.argument("--author").argument(author);
}
// Use:
command.optional_option("--author", maybe_author)
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.
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 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.
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/writingStdio::Inherit - Pass through to parent process (default)Stdio::Null - Redirect to /dev/nullThe 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 OutputEnvironment variable names are validated via EnvVariableName:
= - the OS uses = as separator between name and value; a name containing = corrupts the environment blockstd::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 cmd_proc::{Command, EnvVariableName};
// Compile-time validated (panics at compile time if invalid)
const MY_VAR: EnvVariableName = EnvVariableName::from_static_or_panic("MY_VAR");
Command::new("sh")
.env(&MY_VAR, "value")
.status()?;
// Set multiple variables from an iterator
let vars = [
(EnvVariableName::from_static_or_panic("FOO"), "1"),
(EnvVariableName::from_static_or_panic("BAR"), "2"),
];
Command::new("sh")
.envs(vars)
.status()?;
// Remove a variable from the child environment
const PATH: EnvVariableName = EnvVariableName::from_static_or_panic("PATH");
Command::new("sh")
.env_remove(&PATH)
.status()?;
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());
}
}
}