正在加载,请稍候…

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包

使用 Rust 的 clap derive API 进行参数解析、Tokio 实现异步命令、indicatif 显示进度条、dialoguer 提供交互式提示

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包

Rust 已成为构建 CLI 工具的最佳语言之一。其出色的启动时间、零成本抽象、丰富的生态系统(clap、indicatif、dialoguer)以及便捷的交叉编译能力,使其成为开发工具和系统实用程序的理想选择。本指南涵盖了从参数解析到分发跨平台二进制文件的完整流程。

项目设置

cargo new my-cli-tool
cd my-cli-tool

Cargo.toml:

[package]
name = "my-cli-tool"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "mytool"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
thiserror = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
indicatif = "0.17"
console = "0.15"
dialoguer = "0.11"
dirs = "5"

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包插图

使用 clap 进行参数解析

derive API 提供了一种简洁、声明式的方式来定义 CLI 接口:

use clap::{Parser, Subcommand, Args, ValueEnum};

#[derive(Parser, Debug)]
#[command(
    name = "mytool",
    version,
    about = "一个生产级 CLI 工具",
    long_about = "我的 CLI 工具 - 对文件和 API 执行出色的操作"
)]
struct Cli {
    /// 配置文件路径
    #[arg(short, long, env = "MYTOOL_CONFIG", global = true)]
    config: Option<PathBuf>,
    
    /// 增加详细程度 (-v, -vv, -vvv)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    verbose: u8,
    
    /// 输出格式
    #[arg(long, default_value = "text", global = true)]
    format: OutputFormat,
    
    #[command(subcommand)]
    command: Commands,
}

#[derive(ValueEnum, Debug, Clone)]
enum OutputFormat {
    Text,
    Json,
    Table,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// 处理目录中的文件
    Process(ProcessArgs),
    /// 从 API 获取数据
    Fetch(FetchArgs),
    /// 交互式配置向导
    Configure,
}

#[derive(Args, Debug)]
struct ProcessArgs {
    /// 输入目录
    #[arg(short, long)]
    input: PathBuf,
    
    /// 输出目录
    #[arg(short, long)]
    output: PathBuf,
    
    /// 并行工作线程数
    #[arg(short = 'j', long, default_value = "4")]
    workers: usize,
    
    /// 试运行 - 不写入输出
    #[arg(long)]
    dry_run: bool,
    
    /// 要包含的文件扩展名
    #[arg(long, num_args = 1..)]
    extensions: Vec<String>,
}

#[derive(Args, Debug)]
struct FetchArgs {
    /// API 端点 URL
    url: String,
    
    /// 认证令牌
    #[arg(long, env = "MYTOOL_API_TOKEN")]
    token: Option<String>,
    
    /// 最大重试次数
    #[arg(long, default_value = "3")]
    retries: u32,
    
    /// 请求超时时间(秒)
    #[arg(long, default_value = "30")]
    timeout: u64,
}

异步主入口点

use anyhow::{Context, Result};
use std::path::PathBuf;

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();
    
    // 根据详细程度设置日志级别
    let log_level = match cli.verbose {
        0 => "warn",
        1 => "info",
        2 => "debug",
        _ => "trace",
    };
    
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::new(log_level))
        .init();
    
    // 加载配置
    let config = load_config(cli.config.as_deref()).await
        .context("无法加载配置")?;
    
    match cli.command {
        Commands::Process(args) => process_command(args, &config, cli.format).await,
        Commands::Fetch(args) => fetch_command(args, &config, cli.format).await,
        Commands::Configure => configure_command().await,
    }
}

配置文件管理

use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Serialize, Deserialize, Default)]
struct Config {
    api_token: Option<String>,
    default_workers: Option<usize>,
    output_dir: Option<PathBuf>,
    endpoints: Vec<String>,
}

async fn load_config(path: Option<&Path>) -> Result<Config> {
    let config_path = match path {
        Some(p) => p.to_path_buf(),
        None => {
            let config_dir = dirs::config_dir()
                .ok_or_else(|| anyhow::anyhow!("找不到配置目录"))?;
            config_dir.join("mytool").join("config.toml")
        }
    };
    
    if !config_path.exists() {
        return Ok(Config::default());
    }
    
    let content = tokio::fs::read_to_string(&config_path)
        .await
        .with_context(|| format!("无法读取配置: {}", config_path.display()))?;
    
    toml::from_str(&content)
        .with_context(|| format!("无法解析配置: {}", config_path.display()))
}

async fn save_config(config: &Config, path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }
    let content = toml::to_string_pretty(config)?;
    tokio::fs::write(path, content).await?;
    Ok(())
}

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包插图

使用 indicatif 显示进度条

use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::time::Duration;

async fn process_command(args: ProcessArgs, config: &Config, format: OutputFormat) -> Result<()> {
    let files = collect_files(&args.input, &args.extensions).await?;
    
    let mp = MultiProgress::new();
    let pb = mp.add(ProgressBar::new(files.len() as u64));
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")?
            .progress_chars("#>-"),
    );
    pb.enable_steady_tick(Duration::from_millis(100));
    
    let semaphore = Arc::new(tokio::sync::Semaphore::new(args.workers));
    let pb_clone = pb.clone();
    
    let handles: Vec<_> = files
        .into_iter()
        .map(|file| {
            let sem = Arc::clone(&semaphore);
            let out_dir = args.output.clone();
            let pb = pb_clone.clone();
            
            tokio::spawn(async move {
                let _permit = sem.acquire().await.unwrap();
                let result = process_file(&file, &out_dir).await;
                pb.inc(1);
                result
            })
        })
        .collect();
    
    let mut errors = 0;
    for handle in handles {
        if let Err(e) = handle.await? {
            pb.println(format!("错误: {}", e));
            errors += 1;
        }
    }
    
    pb.finish_with_message(format!("完成!{} 个错误", errors));
    Ok(())
}

交互式对话框

use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};

async fn configure_command() -> Result<()> {
    println!("欢迎使用 mytool 配置向导");
    
    let api_token: String = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("API 令牌")
        .interact()?;
    
    let default_workers: usize = Input::with_theme(&ColorfulTheme::default())
        .with_prompt("默认工作线程数")
        .default(4)
        .validate_with(|input: &usize| {
            if *input > 0 && *input <= 64 {
                Ok(())
            } else {
                Err("工作线程数必须在 1 到 64 之间")
            }
        })
        .interact()?;
    
    let output_formats = vec!["text", "json", "table"];
    let format_idx = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("默认输出格式")
        .items(&output_formats)
        .default(0)
        .interact()?;
    
    let confirmed = Confirm::with_theme(&ColorfulTheme::default())
        .with_prompt("保存配置?")
        .default(true)
        .interact()?;
    
    if confirmed {
        let config = Config {
            api_token: Some(api_token),
            default_workers: Some(default_workers),
            ..Default::default()
        };
        
        let config_path = dirs::config_dir()
            .unwrap()
            .join("mytool")
            .join("config.toml");
        
        save_config(&config, &config_path).await?;
        println!("配置已保存至 {}", config_path.display());
    }
    
    Ok(())
}

错误处理

use thiserror::Error;

#[derive(Error, Debug)]
enum CliError {
    #[error("文件未找到: {path}")]
    FileNotFound { path: PathBuf },
    
    #[error("API 请求失败: {status} - {message}")]
    ApiError { status: u16, message: String },
    
    #[error("配置错误: {0}")]
    Config(String),
    
    #[error("IO 错误: {0}")]
    Io(#[from] std::io::Error),
}

// 使用 anyhow 进行应用级错误处理
async fn fetch_command(args: FetchArgs, config: &Config, format: OutputFormat) -> Result<()> {
    let token = args.token
        .or_else(|| config.api_token.clone())
        .ok_or_else(|| anyhow::anyhow!(
            "未提供 API 令牌。请使用 --token 或设置 MYTOOL_API_TOKEN"
        ))?;
    
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(args.timeout))
        .build()?;
    
    let response = client
        .get(&args.url)
        .header("Authorization", format!("Bearer {}", token))
        .send()
        .await
        .context("发送请求失败")?;
    
    if !response.status().is_success() {
        anyhow::bail!("API 返回 {}: {}", response.status(), response.text().await?);
    }
    
    let data: serde_json::Value = response.json().await?;
    
    match format {
        OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&data)?),
        OutputFormat::Text => println!("{:#?}", data),
        OutputFormat::Table => print_table(&data),
    }
    
    Ok(())
}

用 Rust 构建生产级 CLI 工具:clap、异步与跨平台打包插图

跨平台打包

为多个目标构建:

# 安装交叉编译工具链
cargo install cross

# 为 Linux 构建(从 macOS/Windows)
cross build --release --target x86_64-unknown-linux-gnu

# 为 Windows 构建
cross build --release --target x86_64-pc-windows-gnu

# 为 macOS ARM 构建
cargo build --release --target aarch64-apple-darwin

# 创建压缩发布包
tar czf mytool-linux-x86_64.tar.gz -C target/x86_64-unknown-linux-gnu/release mytool
zip mytool-windows-x86_64.zip target/x86_64-pc-windows-gnu/release/mytool.exe

GitHub Actions 发布工作流:

name: Release
on:
  push:
    tags: ['v*']
jobs:
  build:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact: mytool-linux-x86_64
          - os: macos-latest
            target: aarch64-apple-darwin
            artifact: mytool-macos-arm64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: mytool-windows-x86_64.exe
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - run: cargo build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: target/${{ matrix.target }}/release/mytool*

测试 CLI 命令

#[cfg(test)]
mod tests {
    use assert_cmd::Command;
    use predicates::str::contains;
    
    #[test]
    fn test_help() {
        Command::cargo_bin("mytool")
            .unwrap()
            .arg("--help")
            .assert()
            .success()
            .stdout(contains("一个生产级 CLI 工具"));
    }
    
    #[test]
    fn test_version() {
        Command::cargo_bin("mytool")
            .unwrap()
            .arg("--version")
            .assert()
            .success()
            .stdout(contains(env!("CARGO_PKG_VERSION")));
    }
    
    #[tokio::test]
    async fn test_process_command() {
        let input_dir = tempfile::tempdir().unwrap();
        let output_dir = tempfile::tempdir().unwrap();
        
        Command::cargo_bin("mytool")
            .unwrap()
            .args(["process", "-i", input_dir.path().to_str().unwrap(),
                   "-o", output_dir.path().to_str().unwrap()])
            .assert()
            .success();
    }
}

结论

Rust 是 2026 年构建 CLI 工具的绝佳选择。clap derive API 生成美观、自文档化的参数解析器。Tokio 支持异步命令执行而不阻塞。indicatif 提供生产级进度报告。交叉编译使得分发到所有平台变得简单。结合 Rust 的编译时保证,您将获得快速、可靠且易于使用和维护的 CLI 工具。