cmd-proc - Process Command Builder
A wrapper around tokio::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 (and its async wrapper tokio::process::Command) is powerful but requires verbose boilerplate for common patterns:
async
Key differences from std / tokio process
| Feature | std / tokio process |
cmd-proc |
|---|---|---|
| Debug logging | None | Automatic debug logging of commands before execution |
| Exit code check | Manual | Automatic - non-zero exits return Err |
| Output capture | Returns raw Output struct |
Typestate: .stdout_capture().string(), .stderr_capture().bytes() |
| Builder pattern | Mutable references (&mut self) |
Owned builder with method chaining |
| Stdin data | Requires manual pipe setup | Simple .stdin_bytes() method |
| Env var names | Accepts any AsRef<OsStr> |
EnvVariableName type with compile-time validation |
| Error type | Separate io::Error and ExitStatus |
Unified CommandError with both |
| Process spawning | Direct .spawn() on Command |
.build() for native tokio::process::Command access |
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. - Typestate capture pattern: Stream capture methods transition between builder types (
Command->CaptureSingle<S>->CaptureAll), enforcing valid API usage at compile time. You can't call.bytes()on a double-capture builder, and you can't accidentally forget which stream you're capturing. - Default exit code checking: Capture methods treat non-zero exit as an error by default, preventing accidentally ignored failures. Use
.accept_nonzero_exit()to opt out when needed. - Fluent API: Chain configuration methods naturally without
&mut selfgymnastics.
Usage
async
Stream Capture Typestate
Stream configuration uses a typestate pattern with three builder types. Each method consumes self and returns the appropriate type, enforcing valid transitions at compile time.
State transition matrix
| From | Method | To |
|---|---|---|
Command |
.stdout_capture() |
CaptureSingle<Stdout> |
Command |
.stderr_capture() |
CaptureSingle<Stderr> |
Command |
.stdout_null() |
Command |
Command |
.stderr_null() |
Command |
Command |
.stdout_inherit() |
Command |
Command |
.stderr_inherit() |
Command |
CaptureSingle<Stdout> |
.stderr_capture() |
CaptureAll |
CaptureSingle<Stdout> |
.stderr_null() |
CaptureSingle<Stdout> |
CaptureSingle<Stdout> |
.stderr_inherit() |
CaptureSingle<Stdout> |
CaptureSingle<Stdout> |
.stdout_null() |
Command |
CaptureSingle<Stdout> |
.stdout_inherit() |
Command |
CaptureSingle<Stderr> |
.stdout_capture() |
CaptureAll |
CaptureSingle<Stderr> |
.stdout_null() |
CaptureSingle<Stderr> |
CaptureSingle<Stderr> |
.stdout_inherit() |
CaptureSingle<Stderr> |
CaptureSingle<Stderr> |
.stderr_null() |
Command |
CaptureSingle<Stderr> |
.stderr_inherit() |
Command |
CaptureAll |
.stdout_null() |
CaptureSingle<Stderr> |
CaptureAll |
.stdout_inherit() |
CaptureSingle<Stderr> |
CaptureAll |
.stderr_null() |
CaptureSingle<Stdout> |
CaptureAll |
.stderr_inherit() |
CaptureSingle<Stdout> |
Terminal methods
| Type | Method | Return |
|---|---|---|
Command |
.status() |
Result<(), CommandError> |
Command |
.build() |
tokio::process::Command |
CaptureSingle<S> |
.run() |
Result<CaptureSingleResult, CommandError> |
CaptureSingle<S> |
.bytes() |
Result<Vec<u8>, CommandError> |
CaptureSingle<S> |
.string() |
Result<String, CommandError> |
CaptureAll |
.run() |
Result<CaptureAllResult, CommandError> |
Result types
CaptureSingleResult:bytes: Vec<u8>,status: ExitStatusCaptureAllResult:stdout: Vec<u8>,stderr: Vec<u8>,status: ExitStatus
All builders support .accept_nonzero_exit() to allow non-zero exit codes without error.
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:
async
Native Process Access
For interactive processes or other use cases beyond capture, use .build() to get
the underlying tokio::process::Command:
async
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).
async
Error Handling
CommandError has two mutually exclusive failure modes: an IO error
(command not found, permission denied) or a non-zero exit status — never both.
async