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 fmtclippy -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_parserdefault_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 --checkcargo 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 标识符安全形式。

clap 风格
错误与结构

粘贴后请按需补全子命令体、配置路径与错误码表;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 减小体积

返回技能库 更多技能入口