使用标准库构建 CLI
不引入第三方 clap 时,用 std::env 与 std::io 构建可测试的小型 CLI 闭环。
#howto
#growing
#dev
#rust
[!info] Related notes
使用标准库构建 CLI
目标
完成一个仅依赖标准库的 CLI:解析单个文件路径、读取文件、以非零退出码表达错误,核心逻辑可单测。本例不从 stdin 读取。
这个场景为什么出现
很多内部工具不需要完整解析器;先掌握 std::env::args_os 与纯函数拆分,再上 clap。
[!abstract] 一句话理解
main只做参数与 I/O;纯逻辑放库函数;用退出码与 stderr 表达失败。
最小可运行示例
场景:统计文件字节数的 mini wc -c
use std::env;
use std::fs;
use std::process::ExitCode;
// 业务意图:CLI 统计文件字节数。
// 教学点:args_os 保留平台路径;错误到 stderr;退出码。
fn count_bytes(data: &[u8]) -> usize {
data.len()
}
fn main() -> ExitCode {
let args: Vec<_> = env::args_os().skip(1).collect();
let path = match args.as_slice() {
[arg] if arg == "--help" => {
println!("usage: bytecount [--] <file>");
return ExitCode::SUCCESS;
}
[path] => path,
[flag, path] if flag == "--" => path,
_ => {
eprintln!("usage: bytecount [--] <file>");
return ExitCode::from(2);
}
};
match fs::read(path) {
Ok(bytes) => {
println!("{}", count_bytes(&bytes));
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error reading {}: {e}", path.to_string_lossy());
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_len() {
assert_eq!(count_bytes(b"abc"), 3);
}
}
使用 Rust 1.61+(ExitCode 已稳定)。新建 cargo new bytecount,进入该目录,将代码保存到 src/main.rs;创建内容恰为 abc 的 sample.txt 后运行:
cargo run -- sample.txt # 输出 3
cargo run -- --help # 输出用法
cargo run -- -- --help # 读取文件名为 --help 的文件
cargo test
参数约定:除单独的 --help 外,单个参数按文件路径解释;-- <file> 可显式分隔。缺参或多参退出 2,读文件失败退出 1,成功退出 0。args_os 保留非 UTF-8 路径,只在错误展示时有损转换。返回 ExitCode 允许当前函数正常返回;process::exit 则会跳过栈上析构。
fs::read 一次把整个文件读入内存,适合小文件。这里统计字节而非 Unicode 字符;大文件应按块读取并累计。
结合场景再看三个关注点
- 退出码区分用法错误与运行错误
- 逻辑可测
- 后续可换成 clap 而不改 count_bytes
核心步骤
- 定义用户故事与退出码
- 拆纯函数
- 本例用
args_os与fs::read;按需另设计 stdin 支持 - 测试 +
--help文案(手写)
工程实践
2026-10-07:使用 Rust 1.90.0 提取示例编译,验证普通/空文件、非 UTF-8 路径、缺参、多参、缺失文件、帮助和 -- 分隔;内置单测同时通过。
本节总结
- std 足够做小 CLI
- 纯逻辑与 I/O 分离
- 退出码契约
自测题
- 为什么核心逻辑不要直接读 env?
- 用法错误常用退出码?
参考答案
- 便于测试与复用。
- 常见 2(惯例,可团队约定)。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — I/O Project | 官方书 | CLI 项目 |
| std::env | 标准库 | 参数与环境 |