qubit-command

Command-line process running utilities for Rust

Rust CI Coverage Crates.io Rust License

Command-line process running utilities for Rust.

Overview

Qubit Command provides a small, structured API for running external programs, capturing their output, enforcing timeouts, and reporting command failures with clear error values.

Features

Timeout Behavior

CommandRunner::new() does not enforce a timeout by default. Use timeout(Duration) when a command must be bounded, or without_timeout() when the absence of a timeout should be explicit in builder chains.

When a timeout is configured, the runner attempts to terminate the process tree: Unix commands are spawned in a new process group and Windows commands are spawned in a Job Object.

Large Output

By default stdout and stderr are captured without an in-memory byte limit. For commands that can emit large logs, configure capture limits and tee files:

use qubit_command::{Command, CommandRunner};

let output = CommandRunner::new()
    .max_output_bytes(64 * 1024)
    .tee_stdout_to_file("stdout.log")
    .tee_stderr_to_file("stderr.log")
    .run(Command::new("cargo").arg("test"))?;

if output.stdout_truncated() {
    eprintln!("stdout was truncated in memory; see stdout.log for the full stream");
}
# Ok::<(), Box<dyn std::error::Error>>(())

Quick Start

use std::time::Duration;

use qubit_command::{Command, CommandRunner};

let output = CommandRunner::new()
    .timeout(Duration::from_secs(10))
    .run(Command::new("git").args(&["status", "--short"]))?;

println!("{}", output.stdout_text()?);
# Ok::<(), Box<dyn std::error::Error>>(())

Shell Commands

Prefer structured commands whenever possible:

use qubit_command::{Command, CommandRunner};

let output = CommandRunner::new()
    .run(Command::new("printf").arg("hello"))?;

assert_eq!(output.stdout_text()?, "hello");
# Ok::<(), Box<dyn std::error::Error>>(())

Use Command::shell only when shell parsing, redirection, expansion, or pipes are intentional:

use qubit_command::{Command, CommandRunner};

let output = CommandRunner::new()
    .run(Command::shell("printf hello | tr a-z A-Z"))?;

assert_eq!(output.stdout_text()?, "HELLO");
# Ok::<(), Box<dyn std::error::Error>>(())

Sanitized Diagnostics

Command strings used in runner logs, CommandError::command(), and Command's Debug output are sanitized with qubit-sanitize. Sensitive structured argv values such as --password secret, --access-token=..., and OPENAI_API_KEY=... are masked. Explicit environment overrides are shown only in sanitized form. Command::shell payloads are treated as opaque shell scripts and displayed as <shell command> instead of being parsed.

Add application-specific fields on the runner when the defaults are not enough:

use qubit_command::{Command, CommandRunner, SensitivityLevel};

let error = CommandRunner::new()
    .sensitive_field("tenant_option", SensitivityLevel::Secret)
    .run(Command::new("__missing__").arg("--tenant-option").arg("secret"))
    .expect_err("sample command should fail");

assert_eq!(
    error.command(),
    r#"["__missing__", "--tenant-option", "<redacted>"]"#,
);

Runner-specific fields affect runner logs and CommandError::command(). Standalone Command Debug output has no runner context and uses the built-in defaults only.

Captured stdout/stderr bytes and tee files remain raw process output. Use capture limits and caller-side filtering when command output itself may contain secrets.

Output Text

stdout() and stderr() return raw bytes exactly as retained. Use stdout_text() and stderr_text() when the command output must be valid UTF-8. Use stdout_lossy_text() and stderr_lossy_text() to replace invalid UTF-8 bytes with .

If the captured stdout or stderr contains invalid UTF-8, stdout_text() / stderr_text() return Err(str::Utf8Error) from str::from_utf8. The bytes are still stored on the returned CommandOutput; use stdout() / stderr() to read the raw output and decode or handle it yourself.

use qubit_command::{Command, CommandRunner};

let output = CommandRunner::new()
    .run(Command::shell("printf '\\377'"))?;

assert_eq!(output.stdout_lossy_text(), "\u{fffd}");
# Ok::<(), Box<dyn std::error::Error>>(())

Testing

A minimal local run:

cargo test
cargo clippy --all-targets --all-features -- -D warnings

To mirror what continuous integration enforces, run the repository scripts from the project root: ./align-ci.sh brings local tooling and configuration in line with CI, then ./ci-check.sh runs the same checks the pipeline uses. For test coverage, use ./coverage.sh to generate or open reports (see the script’s help and any project coverage notes for options such as HTML or JSON).

Contributing

Issues and pull requests are welcome.

By contributing, you agree to license your contributions under the Apache License, Version 2.0, the same license as this project.

License

Copyright © 2026 Haixing Hu, Qubit Co. Ltd.

This project is licensed under the Apache License, Version 2.0. See the LICENSE file in the repository for the full text.

Author

Haixing Hu — Qubit Co. Ltd.

Repositorygithub.com/qubit-ltd/rs-command
Documentationdocs.rs/qubit-command
Cratecrates.io/crates/qubit-command