Rust CLI 工具
本页给出可直接运行的 Rust CLI 示例:clap derive 子命令 + flag + arg 完整解析、anyhow/thiserror 错误链、serde JSON 序列化反序列化、Cargo.toml 发布必要字段与 GitHub Actions 跨平台编译矩阵;让 Agent 按 POSIX 惯例实现可脚本化命令行。
SKILL 规定子命令结构、全局 flag(verbose、config)、stdin/stdout 约定与 TTY 检测;人类可读错误走 stderr,机器可读输出可选 JSON 行协议。
二进制体积与 MSRV 若有限制需写明;Cargo.toml 的 feature 与可选依赖用于缩减默认构建。
与 CI 对齐:cargo fmt、clippy -D warnings、跨平台路径(std::path)及 Windows 控制台编码说明。
- 配置:XDG 目录或项目内
.config的优先级与合并规则。 - 测试:
assert_cmd、golden file 与临时目录 fixture。 - 发布:
cargo-dist或类似打包包名、版本与 changelog 片段。
CLI 主流程(skill-flow-block)
[ argv / 环境变量 / 配置文件 ]
│
▼
┌─────────────┐ clap:parse → 校验类型与互斥 flag
│ 参数解析 │──── 失败:stderr 用法提示 + 非零退出(通常 2)
└─────────────┘
│
▼
┌─────────────┐ 业务逻辑;I/O 错误映射为稳定错误码
│ 执行子命令 │──── 日志与进度:TTY 时人类可读,管道时克制 ANSI
└─────────────┘
│
▼
┌─────────────┐ 成功:stdout(或仅 0);失败:stderr + 约定退出码
│ 输出 / 退出 │──── 脚本友好:--json 等机器格式与稳定列顺序
└─────────────┘
先定「成功输出载体」与「失败是否可脚本捕获」再写子命令;避免把堆栈默认打到 stderr(发布构建用 RUST_BACKTRACE 控制)。
clap:子命令、flag 与帮助
derive:适合快速迭代与 Agent 生成;用 #[command(about, long_about)]、value_parser 与 default_value_t 把约束写进类型系统。
- 全局选项:
global = true的-v/--verbose、--config等应在顶层可见于子命令帮助。 - POSIX 习惯:
-表示 stdin/stdout 的语义在 SKILL 中显式写出。 after_help:放示例调用与常见脚本片段。
// Cargo.toml
// [dependencies]
// clap = { version = "4", features = ["derive"] }
// anyhow = "1"
// thiserror = "1"
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use clap::{Parser, Subcommand, Args};
/// 文件处理工具 — POSIX 友好,支持 stdout 输出(-)
#[derive(Parser)]
#[command(
name = "filetool",
version,
about = "文件处理工具",
after_help = "示例:\n filetool convert --format json input.csv\n filetool stats -v ./data/"
)]
struct Cli {
/// 全局 verbose(所有子命令可见)
#[arg(short, long, global = true)]
verbose: bool,
/// 配置文件路径(默认 ~/.config/filetool/config.toml)
#[arg(long, global = true, value_name = "FILE")]
config: Option,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 转换文件格式
Convert(ConvertArgs),
/// 统计目录信息
Stats {
/// 目录路径
#[arg(default_value = ".")]
path: std::path::PathBuf,
/// 递归深度
#[arg(short, long, default_value_t = 1)]
depth: u32,
},
}
#[derive(Args)]
struct ConvertArgs {
/// 输入文件(- 表示 stdin)
input: String,
/// 输出格式
#[arg(short, long, value_parser = ["json", "csv", "toml"])]
format: String,
/// 输出文件(- 表示 stdout,默认)
#[arg(short, long, default_value = "-")]
output: String,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match &cli.command {
Commands::Convert(args) => run_convert(args, cli.verbose),
Commands::Stats { path, depth } => run_stats(path, *depth, cli.verbose),
}
}
fn run_convert(args: &ConvertArgs, verbose: bool) -> anyhow::Result<()> {
if verbose {
eprintln!("[verbose] converting {} → {}", args.input, args.format);
}
// ... 实现
Ok(())
}
fn run_stats(path: &std::path::Path, depth: u32, _verbose: bool) -> anyhow::Result<()> {
anyhow::ensure!(path.exists(), "路径不存在: {}", path.display());
println!("stats: {:?} depth={}", path, depth);
Ok(())
}
错误、退出码与 stderr/stdout
anyhow
- 二进制顶层:
main() -> anyhow::Result<()>,上下文用.context()/with_context()。 - 适合「报告给用户一段话」;不保证稳定错误分类。
thiserror
- 库或可测试边界:枚举变体对应业务失败原因,可实现
std::process::Termination或映射到退出码。 - 与
clap::error::ErrorKind区分:用法错误不要混成 I/O 错误码。
约定文档列出:0 成功、1 一般失败、2 用法/clap 解析错误(与常见工具一致)、其它专码(如「需登录」)。
// anyhow/thiserror 错误链 + serde JSON 序列化示例
use thiserror::Error;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// 可程序分类的错误类型(thiserror)
#[derive(Debug, Error)]
pub enum AppError {
#[error("配置文件读取失败: {path}")]
ConfigRead { path: PathBuf, #[source] source: std::io::Error },
#[error("JSON 解析失败: {0}")]
JsonParse(#[from] serde_json::Error),
#[error("认证失败,请先登录")]
Unauthorized,
}
impl AppError {
/// 映射到 POSIX 退出码:0=成功, 1=一般失败, 2=用法, 3=认证
pub fn exit_code(&self) -> i32 {
match self {
AppError::Unauthorized => 3,
_ => 1,
}
}
}
/// serde JSON 序列化/反序列化
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub api_url: String,
pub timeout_secs: u64,
#[serde(default = "default_retries")]
pub max_retries: u32,
}
fn default_retries() -> u32 { 3 }
pub fn load_config(path: &PathBuf) -> Result<Config, AppError> {
let data = std::fs::read_to_string(path)
.map_err(|e| AppError::ConfigRead { path: path.clone(), source: e })?;
// JsonParse 通过 #[from] 自动转换
let cfg: Config = serde_json::from_str(&data)?;
Ok(cfg)
}
/// main 中统一处理退出码(错误输出到 stderr)
fn main() {
if let Err(e) = run() {
eprintln!("error: {e:#}"); // anyhow 链式格式
std::process::exit(1);
}
}
fn run() -> anyhow::Result<()> {
let path = PathBuf::from("config.json");
let cfg = load_config(&path)
.map_err(|e| anyhow::anyhow!("{e}"))
.context("初始化配置失败")?;
// 输出 JSON 到 stdout(机器可读)
println!("{}", serde_json::to_string_pretty(&cfg)?);
Ok(())
}
stdin、TTY 与机器可读输出
用 is_terminal::is_terminal 或同类检测 stdout/stderr 是否为 TTY;彩色与进度条仅在 TTY 开启。管道场景避免交互式确认。
- 结构化输出:JSON Lines 或单一 JSON 对象;字段名与排序稳定,便于
jq。 - 日志:若引入
tracing,说明默认级别与环境变量覆盖。
测试与 CI(fmt / clippy)
cargo fmt --check、cargo clippy -- -D warnings作为合流门禁;MSRV 在 CI 矩阵中单独 job。- 集成测试:
assert_cmd断言退出码与 stderr 片段;fixture 用tempfile。 - 跨平台:路径用
PathBuf,必要时在 SKILL 中注明 Windows 路径与 UTF-8 假设。
# Cargo.toml 发布必要字段
[package]
name = "filetool"
version = "0.3.1"
edition = "2021"
rust-version = "1.74" # MSRV:CI 矩阵中用此版本单独跑
description = "文件格式转换工具"
license = "MIT OR Apache-2.0"
repository = "https://github.com/acme/filetool"
keywords = ["cli", "file", "convert"]
categories = ["command-line-utilities"]
readme = "README.md"
[[bin]]
name = "filetool"
path = "src/main.rs"
[profile.release]
strip = true # 减小二进制体积
lto = true
codegen-units = 1
# .github/workflows/ci.yml — 跨平台编译 GitHub Actions 矩阵
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: rustfmt, clippy }
- run: cargo fmt --check
- run: cargo clippy -- -D warnings
- run: cargo test
build-matrix:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
use_cross: true
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { targets: "${{ matrix.target }}" }
- name: Build (cross)
if: matrix.use_cross
run: |
cargo install cross --git https://github.com/cross-rs/cross
cross build --release --target ${{ matrix.target }}
- name: Build (native)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: filetool-${{ matrix.target }}
path: target/${{ matrix.target }}/release/filetool*
Cargo 与 main 骨架实验室(rust-page JS)
勾选依赖与 API 风格,生成可粘贴的 Cargo.toml 片段与 src/main.rs 提纲;crate 名会规范为 Rust 标识符安全形式。
粘贴后请按需补全子命令体、配置路径与错误码表;builder 分支仅给出 Command::new 方向的注释步骤,避免在此页生成过长动态代码。
---
name: rust-cli-tool
description: 用 clap derive、anyhow/thiserror 与 serde 实现可发布的 Rust CLI
---
# 参数解析
- clap derive:Parser + Subcommand + Args 分层;global = true 的全局 flag
- value_parser 约束合法值;- 表示 stdin/stdout 在 SKILL 中显式说明
- after_help 放示例调用片段,便于用户快速上手
# 错误处理
- 二进制 main() -> anyhow::Result<()>;.context() 添加调用栈上下文
- 库/边界用 thiserror 枚举;变体与退出码映射表写进文档
- 退出码约定:0=成功, 1=一般失败, 2=用法错误(clap 默认), 3+=业务专码
- 错误信息输出 stderr;机器可读输出走 stdout JSON
# 序列化
- serde derive + serde_json:Config/Output 结构体加 Serialize/Deserialize
- 可选字段用 #[serde(default)] 保持向后兼容;输出字段顺序固定便于 jq
# 发布检查清单
- Cargo.toml:name/version/rust-version/description/license/repository 齐全
- cargo fmt --check + cargo clippy -- -D warnings 作为 CI 门禁
- cargo test(含集成测试 assert_cmd 验证退出码与 stderr 片段)
- 跨平台矩阵:linux-x64, linux-arm64(cross), macos-x64, macos-arm64, windows
- profile.release: strip=true, lto=true 减小体积