cmd-proc 0.1.0

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.

> **Status**: Pre-1.0 - exists to serve [mbj/mrs]https://github.com/mbj/mrs monorepo, expect breaking changes without notice.

## Why cmd-proc?

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

```rust
// 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

```rust
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()?;
```

## 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:

```rust
// 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:

```rust
// Instead of:
if let Some(author) = maybe_author {
    command = command.argument("--author").argument(author);
}

// Use:
command.optional_option("--author", maybe_author)
```

## The Capture Pattern

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

```rust
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()`:

```rust
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()`:

```rust
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 (when `from_static_or_panic` is used in a `const` context) or parse time (`FromStr`).

```rust
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()?;
```

## Error Handling

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

```rust
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());
        }
    }
}
```