Chapter 09

实用工具开发

用 Rust 构建生产级 CLI 工具——命令行解析、文件处理、JSON 序列化一站式实战

命令行参数:clap

为什么用 clap?

clap 是 Rust 生态中最流行的命令行参数解析库,被 ripgrep(rg)、bat、cargo 等知名工具使用。它能自动生成帮助文档、验证参数类型、支持子命令,并且使用声明式宏或 derive 宏,代码非常简洁。

[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

/// 一个简单的文件处理工具
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// 要处理的文件路径
    path: std::path::PathBuf,

    /// 显示行号
    #[arg(short = 'n', long)]
    line_numbers: bool,

    /// 过滤包含此字符串的行
    #[arg(short, long)]
    filter: Option<String>,

    /// 最多显示的行数
    #[arg(short, long, default_value_t = 100)]
    max_lines: usize,
}

fn main() {
    let cli = Cli::parse();
    println!("处理文件: {:?}", cli.path);
    println!("行号: {}, 过滤: {:?}, 最多行: {}",
        cli.line_numbers, cli.filter, cli.max_lines);
}

子命令(Subcommands)

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "mytool")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 添加条目
    Add {
        /// 条目内容
        content: String,
    },
    /// 列出所有条目
    List {
        #[arg(short, long)]
        verbose: bool,
    },
    /// 删除条目
    Remove { id: u32 },
}

fn main() {
    let cli = Cli::parse();
    match cli.command {
        Commands::Add { content }     => println!("添加: {}", content),
        Commands::List { verbose }    => println!("列出(详细: {})", verbose),
        Commands::Remove { id }       => println!("删除 #{}", id),
    }
}

文件 I/O

读写文件的多种方式

use std::fs;
use std::io::{self, BufRead, Write};
use std::path::Path;

fn file_examples() -> io::Result<()> {
    // 一次性读取全部内容
    let content = fs::read_to_string("input.txt")?;
    println!("文件内容:\n{}", content);

    // 读取为字节
    let bytes = fs::read("binary.bin")?;
    println!("字节数: {}", bytes.len());

    // 一次性写入(覆盖)
    fs::write("output.txt", "Hello, Rust!\n")?;

    // 追加写入
    use fs::OpenOptions;
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)
        .open("log.txt")?;
    writeln!(file, "新的日志条目")?;

    Ok(())
}

// 逐行读取大文件(内存高效)
fn read_lines(filename: &str) -> io::Result<Vec<String>> {
    let file = fs::File::open(filename)?;
    let reader = io::BufReader::new(file);
    reader.lines().collect()
}

路径操作

use std::path::{Path, PathBuf};

fn path_examples() {
    let path = Path::new("/home/user/projects/rust/src/main.rs");

    println!("文件名: {:?}",   path.file_name());    // "main.rs"
    println!("扩展名: {:?}",   path.extension());   // "rs"
    println!("父目录: {:?}",   path.parent());      // .../src
    println!("是否存在: {}", path.exists());
    println!("是否是文件: {}", path.is_file());

    // PathBuf:可变路径
    let mut buf = PathBuf::from("/home/user");
    buf.push("projects");
    buf.push("rust");
    println!("{:?}", buf);  // /home/user/projects/rust

    // 遍历目录
    if let Ok(entries) = std::fs::read_dir(".") {
        for entry in entries.flatten() {
            println!("{:?}", entry.file_name());
        }
    }
}

序列化:serde + JSON

serde:Rust 序列化的标准

serde 是 Rust 生态中几乎无处不在的序列化/反序列化框架,支持 JSON、TOML、YAML、MessagePack 等数十种格式,且通过过程宏零样板代码地实现。

[dependencies]
serde      = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
    #[serde(default)]            // 缺失时使用默认值
    active: bool,
    #[serde(skip_serializing_if = "Option::is_none")]  // 为 None 时不序列化
    avatar: Option<String>,
    #[serde(rename = "createdAt")] // JSON 中用驼峰命名
    created_at: String,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        id: 1,
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
        active: true,
        avatar: None,
        created_at: String::from("2024-01-01"),
    };

    // 序列化为 JSON 字符串
    let json = serde_json::to_string_pretty(&user)?;
    println!({}, json);

    // 反序列化:JSON 字符串 → Rust 结构体
    let json_str = r#"{"id":2,"name":"Bob","email":"bob@example.com","createdAt":"2024-02-01"}"#;
    let user2: User = serde_json::from_str(json_str)?;
    println!("{:?}", user2);

    // 序列化为文件
    let file = std::fs::File::create("user.json")
        .map_err(|e| serde_json::Error::io(e))?;
    serde_json::to_writer_pretty(file, &user)?;

    Ok(())
}

处理动态 JSON

use serde_json::{Value, json};

fn dynamic_json() {
    // json! 宏创建动态 JSON 值
    let data = json!({
        "name": "Rust",
        "version": 2024,
        "features": ["safety", "speed", "concurrency"]
    });

    // 动态访问(注意:可能返回 None)
    if let Some(name) = data["name"].as_str() {
        println!("语言: {}", name);
    }

    // 遍历数组
    if let Some(features) = data["features"].as_array() {
        for f in features {
            println!("特性: {}", f);
        }
    }
}

完整 CLI 工具实战

构建一个 TODO 管理工具

[dependencies]
clap      = { version = "4", features = ["derive"] }
serde     = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow    = "1"
use anyhow::Result;
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

const DATA_FILE: &str = "todos.json";

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

#[derive(Parser)]
#[command(name = "todo", about = "TODO 任务管理工具")]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// 添加新任务
    Add { title: String },
    /// 列出所有任务
    List,
    /// 标记任务完成
    Done { id: u32 },
    /// 删除任务
    Remove { id: u32 },
}

fn load_todos() -> Result<Vec<Todo>> {
    if !PathBuf::from(DATA_FILE).exists() {
        return Ok(vec![]);
    }
    let content = fs::read_to_string(DATA_FILE)?;
    Ok(serde_json::from_str(&content)?)
}

fn save_todos(todos: &[Todo]) -> Result<()> {
    fs::write(DATA_FILE, serde_json::to_string_pretty(todos)?)?;
    Ok(())
}

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

    match cli.command {
        Command::Add { title } => {
            let id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;
            todos.push(Todo { id, title: title.clone(), done: false });
            save_todos(&todos)?;
            println!("✓ 已添加 #{}: {}", id, title);
        }
        Command::List => {
            if todos.is_empty() {
                println!("暂无任务");
            } else {
                for t in &todos {
                    let mark = if t.done { "✓" } else { "○" };
                    println!("[{}] #{}: {}", mark, t.id, t.title);
                }
            }
        }
        Command::Done { id } => {
            if let Some(t) = todos.iter_mut().find(|t| t.id == id) {
                t.done = true;
                save_todos(&todos)?;
                println!("✓ 已完成 #{}", id);
            } else {
                println!("未找到 #{}", id);
            }
        }
        Command::Remove { id } => {
            let before = todos.len();
            todos.retain(|t| t.id != id);
            if todos.len() < before {
                save_todos(&todos)?;
                println!("✓ 已删除 #{}", id);
            } else {
                println!("未找到 #{}", id);
            }
        }
    }
    Ok(())
}
Rust CLI 工具的性能优势

Rust 编写的 CLI 工具启动时间通常在毫秒级别,内存占用极低。相比之下,Python 工具启动时需要加载解释器(通常需要数十毫秒到数百毫秒),Java/Node.js 工具需要 JVM/V8 初始化(数百毫秒甚至更长)。这使得 Rust 成为编写高频调用的系统工具的首选语言。