qubit-command

Command-line process running utilities for Rust

Rust CI Coverage Crates.io Rust License

面向 Rust 的命令行进程运行工具库。

概览

Qubit Command 提供一个小而明确的结构化 API,用于运行外部程序、捕获 stdout/stderr、控制超时,并用清晰的错误类型报告命令执行失败。

功能

超时行为

CommandRunner::new() 默认不限制执行时间。需要约束命令运行时长时,请显式调用 timeout(Duration);如果希望在 builder 链中明确表达不设超时,可以调用 without_timeout()

设置超时后,runner 会尝试终止整个进程树:Unix 平台把命令放入新的 process group,Windows 平台把命令放入 Job Object。

大输出

默认情况下 stdout 和 stderr 的内存捕获不设字节上限。如果命令可能输出大量日志, 可以同时设置捕获上限和 tee 文件:

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>>(())

快速开始

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 命令

优先使用结构化命令:

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>>(())

只有在明确需要 shell 解析、重定向、变量展开或管道时,才使用 Command::shell

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>>(())

诊断脱敏

Runner 日志、CommandError::command()CommandDebug 输出都会通过 qubit-sanitize 生成脱敏命令文本。类似 --password secret--access-token=...OPENAI_API_KEY=... 的结构化 argv 值会被遮蔽;显式设置的 环境变量覆盖也只展示脱敏后的 KEY=valueCommand::shell 的脚本体不做 shell 语法解析,统一作为不透明脚本显示为 <shell command>

当默认敏感字段不够时,可以在 runner 上追加业务字段:

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 上追加的字段只影响 runner 日志和 CommandError::command()。 独立的 Command Debug 输出没有 runner 上下文,只使用内置默认字段。

捕获到的 stdout/stderr 字节以及 tee 文件仍然是进程原始输出。如果命令输出本身可能包含 敏感信息,请配置捕获上限,并在调用方按业务语义过滤。

输出文本

stdout()stderr() 返回保留下来的原始字节。需要严格 UTF-8 文本时, 使用 stdout_text()stderr_text();需要把非法 UTF-8 字节替换成 时,使用 stdout_lossy_text()stderr_lossy_text()

若实际捕获的 stdout 或 stderr 中含有非法 UTF-8 序列,则 stdout_text() / stderr_text() 会分别返回 Err(str::Utf8Error)(内部对保留字节执行 str::from_utf8 失败),无法得到 &str;此时输出仍已完整保留在 CommandOutput 中,可改用 stdout() / stderr() 取得原始字节并自行处理。

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>>(())

测试

快速在本地跑一遍:

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

若要与持续集成(CI)保持一致,请在仓库根目录依次执行:./align-ci.sh 将本地工具链与配置对齐到 CI 规则,再执行 ./ci-check.sh 复现流水线中的检查。需要查看或生成测试覆盖率时,使用 ./coverage.sh(具体参数与输出形式见脚本说明及项目内与覆盖率相关的说明)。

参与贡献

欢迎通过 Issue 与 Pull Request 参与本仓库。建议:

向本仓库贡献内容即表示您同意以 Apache License, Version 2.0(与本项目相同)授权您的贡献。

许可证与版权

版权所有 © 2026 Haixing Hu,Qubit Co. Ltd.。

本软件依据 Apache License, Version 2.0 授权;完整许可文本见仓库根目录的 LICENSE 文件。

作者与维护

Haixing Hu — Qubit Co. Ltd.

源码仓库github.com/qubit-ltd/rs-command
API 文档docs.rs/qubit-command
Crate 发布crates.io/crates/qubit-command