进阶3-4 小时·工具开发

用 Rust 构建高性能 CLI 工具

使用 clap + serde + anyhow 构建一个带参数解析、配置文件、错误处理的命令行工具

Rustcliclapserdeerror-handling

用 Rust 构建高性能 CLI 工具

使用 clap + serde + anyhow 构建一个带参数解析、配置文件、错误处理的命令行工具

你将学到

  • 使用 clap 定义命令行参数和子命令
  • 使用 serde 读写配置文件
  • 使用 anyhow 进行错误处理
  • 构建一个完整的 CLI 应用

前置知识

架构设计

rust-cli/
├── src/
│   ├── main.rs         # 入口
│   ├── cli.rs          # 命令行定义
│   ├── config.rs       # 配置管理
│   └── commands/       # 子命令
│       ├── mod.rs
│       ├── init.rs
│       └── run.rs
├── Cargo.toml
└── config.toml

实现步骤

第一步:项目初始化

cargo new rust-cli
cd rust-cli
# Cargo.toml
[package]
name = "rust-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
anyhow = "1"

第二步:定义命令行参数

// src/cli.rs
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "rust-cli")]
#[command(about = "一个高性能的命令行工具")]
#[command(version)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,

    /// 配置文件路径
    #[arg(short, long, default_value = "config.toml")]
    pub config: String,

    /// 启用详细输出
    #[arg(short, long)]
    pub verbose: bool,
}

#[derive(Subcommand)]
pub enum Commands {
    /// 初始化项目
    Init {
        /// 项目名称
        name: String,
    },
    /// 运行任务
    Run {
        /// 任务名称
        #[arg(short, long)]
        task: String,

        /// 并发数
        #[arg(short, long, default_value = "4")]
        workers: usize,
    },
    /// 显示配置
    Config,
}

第三步:配置管理

// src/config.rs
use serde::{Deserialize, Serialize};
use std::path::Path;
use anyhow::{Result, Context};

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
    pub project: ProjectConfig,
    pub runtime: RuntimeConfig,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectConfig {
    pub name: String,
    pub version: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RuntimeConfig {
    pub workers: usize,
    pub timeout: u64,
}

impl Config {
    pub fn load(path: &str) -> Result<Self> {
        let content = std::fs::read_to_string(path)
            .context(format!("无法读取配置文件: {}", path))?;

        let config: Config = toml::from_str(&content)
            .context("解析配置文件失败")?;

        Ok(config)
    }

    pub fn save(&self, path: &str) -> Result<()> {
        let content = toml::to_string_pretty(self)?;
        std::fs::write(path, content)?;
        Ok(())
    }

    pub fn default() -> Self {
        Config {
            project: ProjectConfig {
                name: "my-project".to_string(),
                version: "0.1.0".to_string(),
            },
            runtime: RuntimeConfig {
                workers: 4,
                timeout: 30,
            },
        }
    }
}

第四步:实现子命令

// src/commands/init.rs
use anyhow::Result;
use std::fs;
use crate::config::Config;

pub fn execute(name: &str) -> Result<()> {
    println!("初始化项目: {}", name);

    // 创建目录
    fs::create_dir_all(name)?;

    // 创建配置文件
    let config = Config {
        project: crate::config::ProjectConfig {
            name: name.to_string(),
            version: "0.1.0".to_string(),
        },
        ..Config::default()
    };

    config.save(&format!("{}/config.toml", name))?;

    println!("✅ 项目初始化完成");
    Ok(())
}
// src/commands/run.rs
use anyhow::Result;
use crate::config::Config;

pub fn execute(task: &str, workers: usize, config: &Config) -> Result<()> {
    println!("运行任务: {}", task);
    println!("并发数: {}", workers);
    println!("超时: {}s", config.runtime.timeout);

    // 模拟任务执行
    for i in 0..workers {
        println!("  Worker {} 处理中...", i);
    }

    println!("✅ 任务完成");
    Ok(())
}

第五步:主程序

// src/main.rs
mod cli;
mod config;
mod commands;

use clap::Parser;
use cli::{Cli, Commands};
use config::Config;
use anyhow::Result;

fn main() -> Result<()> {
    let cli = Cli::parse();

    // 加载配置
    let config = if std::path::Path::new(&cli.config).exists() {
        Config::load(&cli.config)?
    } else {
        Config::default()
    };

    if cli.verbose {
        println!("配置: {:?}", config);
    }

    // 执行命令
    match cli.command {
        Commands::Init { name } => {
            commands::init::execute(&name)?;
        }
        Commands::Run { task, workers } => {
            commands::run::execute(&task, workers, &config)?;
        }
        Commands::Config => {
            println!("{:#?}", config);
        }
    }

    Ok(())
}
// src/commands/mod.rs
pub mod init;
pub mod run;

完整项目结构

rust-cli/
├── src/
│   ├── main.rs         # 程序入口
│   ├── cli.rs          # clap 命令行定义
│   ├── config.rs       # serde 配置管理
│   └── commands/
│       ├── mod.rs
│       ├── init.rs     # init 子命令
│       └── run.rs      # run 子命令
├── Cargo.toml
└── config.toml

最佳实践

  1. 使用 clap derive:比 builder 模式更简洁
  2. anyhow 统一错误处理:避免到处写 Box<dyn Error>
  3. serde 序列化:轻松处理配置文件
  4. 模块化设计:每个子命令独立模块
  5. 完善的帮助信息:clap 自动生成

常见问题

Q: 如何支持多个配置文件格式? A: 使用 serde 支持多种格式(toml/json/yaml),根据扩展名选择。

Q: 如何处理子命令的参数冲突? A: 使用 clap 的 conflicts_with 属性。

Q: 如何添加 Shell 自动补全? A: 使用 clap_complete crate。

扩展挑战

  1. 添加 Shell 自动补全
  2. 实现插件系统
  3. 添加进度条显示

相关课程

课程相关章节
Rust结构体(Struct)
Rust错误处理:Result 与 Option
Rust常用集合:Vec, String, HashMap
Rust模块系统与可见性