第21章 构建命令行工具

文章目录

第21章 构建命令行工具

命令行工具是系统编程和日常开发中不可或缺的一部分。Rust凭借其出色的性能、内存安全性和强大的生态系统,成为了构建命令行工具的绝佳选择。本章将全面介绍如何使用Rust构建功能完整、用户友好的命令行应用程序。

21.1 接受命令行参数

命令行参数是用户与工具交互的主要方式。Rust提供了多种处理命令行参数的方法,从标准库的基础功能到功能丰富的第三方库。

使用标准库处理参数

Rust标准库提供了基本的命令行参数处理功能,适合简单的用例。

rust 复制代码
use std::env;

// 基本的命令行参数解析
fn basic_args() {
    let args: Vec<String> = env::args().collect();
    
    println!("程序名称: {}", args[0]);
    
    match args.len() {
        1 => println!("没有提供参数"),
        2 => println!("有一个参数: {}", args[1]),
        _ => {
            println!("有多个参数:");
            for (i, arg) in args.iter().skip(1).enumerate() {
                println!("  {}: {}", i + 1, arg);
            }
        }
    }
}

// 更结构化的参数解析
struct CliConfig {
    input_file: String,
    output_file: Option<String>,
    verbose: bool,
    count: usize,
}

impl CliConfig {
    fn from_args() -> Result<Self, String> {
        let args: Vec<String> = env::args().collect();
        
        if args.len() < 2 {
            return Err("用法: program <输入文件> [输出文件] [-v|--verbose] [-c|--count N]".to_string());
        }
        
        let mut config = CliConfig {
            input_file: args[1].clone(),
            output_file: None,
            verbose: false,
            count: 1,
        };
        
        let mut i = 2;
        while i < args.len() {
            match args[i].as_str() {
                "-v" | "--verbose" => {
                    config.verbose = true;
                    i += 1;
                }
                "-c" | "--count" => {
                    if i + 1 >= args.len() {
                        return Err("--count 参数需要提供一个数值".to_string());
                    }
                    config.count = args[i + 1].parse().map_err(|_| "无效的计数值".to_string())?;
                    i += 2;
                }
                _ => {
                    if config.output_file.is_none() {
                        config.output_file = Some(args[i].clone());
                    } else {
                        return Err(format!("未知参数: {}", args[i]));
                    }
                    i += 1;
                }
            }
        }
        
        Ok(config)
    }
}

// 演示标准库参数解析
fn demonstrate_std_args() {
    println!("=== 基本参数解析 ===");
    basic_args();
    
    println!("\n=== 结构化参数解析 ===");
    match CliConfig::from_args() {
        Ok(config) => {
            println!("输入文件: {}", config.input_file);
            if let Some(output) = config.output_file {
                println!("输出文件: {}", output);
            }
            println!("详细模式: {}", config.verbose);
            println!("计数: {}", config.count);
        }
        Err(e) => {
            eprintln!("错误: {}", e);
        }
    }
}

fn main() {
    demonstrate_std_args();
}

使用 clap 库进行高级参数解析

clap 是 Rust 生态系统中最流行的命令行参数解析库,提供了声明式和编程式两种 API。

首先在 Cargo.toml 中添加依赖:

toml 复制代码
[dependencies]
clap = { version = "4.0", features = ["derive"] }
rust 复制代码
use clap::{Parser, Subcommand, Arg, ArgAction, value_parser};

// 使用派生宏的声明式 API
#[derive(Parser, Debug)]
#[command(name = "rcli", version = "1.0", about = "一个强大的 Rust CLI 工具", long_about = None)]
struct Cli {
    /// 输入文件路径
    #[arg(short, long, value_name = "FILE")]
    input: String,
    
    /// 输出文件路径
    #[arg(short, long, value_name = "FILE")]
    output: Option<String>,
    
    /// 启用详细输出
    #[arg(short, long, action = ArgAction::SetTrue)]
    verbose: bool,
    
    /// 处理次数
    #[arg(short, long, default_value_t = 1, value_parser = value_parser!(u8).range(1..=100))]
    count: u8,
    
    /// 子命令
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// 处理配置文件
    Config {
        /// 配置文件路径
        #[arg(short, long)]
        file: String,
        
        /// 操作类型
        #[arg(value_enum)]
        action: ConfigAction,
    },
    /// 网络相关操作
    Network {
        /// 主机地址
        #[arg(short, long)]
        host: String,
        
        /// 端口号
        #[arg(short, long, default_value_t = 8080)]
        port: u16,
    },
}

#[derive(clap::ValueEnum, Clone, Debug)]
enum ConfigAction {
    /// 验证配置
    Validate,
    /// 生成配置
    Generate,
    /// 显示配置
    Show,
}

// 编程式 API
fn build_clap_app() -> clap::Command {
    clap::Command::new("rcli")
        .version("1.0")
        .about("一个强大的 Rust CLI 工具")
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .value_name("FILE")
                .help("输入文件路径")
                .required(true)
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("输出文件路径")
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("启用详细输出")
                .action(ArgAction::SetTrue)
        )
        .arg(
            Arg::new("count")
                .short('c')
                .long("count")
                .value_name("COUNT")
                .help("处理次数")
                .default_value("1")
                .value_parser(value_parser!(u8).range(1..=100))
        )
        .subcommand(
            clap::Command::new("config")
                .about("处理配置文件")
                .arg(
                    Arg::new("file")
                        .short('f')
                        .long("file")
                        .value_name("FILE")
                        .help("配置文件路径")
                        .required(true)
                )
                .arg(
                    Arg::new("action")
                        .value_name("ACTION")
                        .help("操作类型")
                        .value_parser(["validate", "generate", "show"])
                        .required(true)
                )
        )
}

// 演示 clap 功能
fn demonstrate_clap() {
    println!("=== 使用派生宏 API ===");
    
    let cli = Cli::parse();
    
    println!("输入文件: {}", cli.input);
    if let Some(output) = cli.output {
        println!("输出文件: {}", output);
    }
    println!("详细模式: {}", cli.verbose);
    println!("计数: {}", cli.count);
    
    if let Some(command) = cli.command {
        match command {
            Commands::Config { file, action } => {
                println!("配置命令 - 文件: {}, 操作: {:?}", file, action);
            }
            Commands::Network { host, port } => {
                println!("网络命令 - 主机: {}, 端口: {}", host, port);
            }
        }
    }
    
    println!("\n=== 使用编程式 API ===");
    
    let matches = build_clap_app().get_matches();
    
    if let Some(input) = matches.get_one::<String>("input") {
        println!("输入文件: {}", input);
    }
    
    if matches.get_flag("verbose") {
        println!("详细模式已启用");
    }
    
    if let Some(count) = matches.get_one::<u8>("count") {
        println!("计数: {}", count);
    }
    
    if let Some(matches) = matches.subcommand_matches("config") {
        if let Some(file) = matches.get_one::<String>("file") {
            println!("配置文件: {}", file);
        }
        if let Some(action) = matches.get_one::<String>("action") {
            println!("配置操作: {}", action);
        }
    }
}

fn main() {
    // 在实际使用时,取消注释下面的行
    // demonstrate_clap();
    
    // 演示标准库参数解析
    demonstrate_std_args();
}

参数验证和转换

对命令行参数进行验证和类型转换是构建健壮 CLI 工具的重要环节。

rust 复制代码
use std::path::{Path, PathBuf};
use std::net::IpAddr;

// 自定义验证器
fn validate_file_exists(s: &str) -> Result<PathBuf, String> {
    let path = Path::new(s);
    if path.exists() {
        Ok(path.to_path_buf())
    } else {
        Err(format!("文件不存在: {}", s))
    }
}

fn validate_port(s: &str) -> Result<u16, String> {
    s.parse::<u16>()
        .map_err(|_| format!("无效的端口号: {}", s))
        .and_then(|port| {
            if port > 0 {
                Ok(port)
            } else {
                Err("端口号必须大于 0".to_string())
            }
        })
}

fn validate_ip_address(s: &str) -> Result<IpAddr, String> {
    s.parse::<IpAddr>()
        .map_err(|_| format!("无效的 IP 地址: {}", s))
}

// 高级参数配置
#[derive(Parser, Debug)]
struct AdvancedCli {
    /// 输入文件路径
    #[arg(
        short, 
        long, 
        value_name = "FILE",
        value_parser = validate_file_exists
    )]
    input: PathBuf,
    
    /// 监听端口
    #[arg(
        short, 
        long,
        value_parser = validate_port
    )]
    port: u16,
    
    /// 服务器地址
    #[arg(
        short,
        long,
        value_parser = validate_ip_address
    )]
    host: IpAddr,
    
    /// 日志级别
    #[arg(
        short,
        long,
        value_enum,
        default_value_t = LogLevel::Info
    )]
    log_level: LogLevel,
    
    /// 配置文件路径
    #[arg(
        short = 'C',
        long,
        value_name = "CONFIG_FILE",
        env = "MYAPP_CONFIG"  // 可以从环境变量读取
    )]
    config: Option<PathBuf>,
}

#[derive(clap::ValueEnum, Clone, Debug)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

impl std::fmt::Display for LogLevel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LogLevel::Error => write!(f, "ERROR"),
            LogLevel::Warn => write!(f, "WARN"),
            LogLevel::Info => write!(f, "INFO"),
            LogLevel::Debug => write!(f, "DEBUG"),
            LogLevel::Trace => write!(f, "TRACE"),
        }
    }
}

fn demonstrate_advanced_args() {
    println!("=== 高级参数验证和转换 ===");
    
    // 在实际使用中,我们会解析真实参数
    // let cli = AdvancedCli::parse();
    
    // 演示验证函数
    match validate_file_exists("Cargo.toml") {
        Ok(path) => println!("文件存在: {:?}", path),
        Err(e) => println!("错误: {}", e),
    }
    
    match validate_port("8080") {
        Ok(port) => println!("有效端口: {}", port),
        Err(e) => println!("错误: {}", e),
    }
    
    match validate_ip_address("127.0.0.1") {
        Ok(ip) => println!("有效 IP: {}", ip),
        Err(e) => println!("错误: {}", e),
    }
}

fn main() {
    demonstrate_advanced_args();
}

21.2 读取文件和错误处理

文件操作和错误处理是 CLI 工具的核心功能。Rust 的所有权系统和错误处理机制让这些操作既安全又高效。

基本文件操作

rust 复制代码
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Write, Read, Seek, SeekFrom};
use std::path::Path;

// 读取文件内容
fn read_file_contents(path: &Path) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// 逐行读取文件
fn read_file_lines(path: &Path) -> io::Result<Vec<String>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    reader.lines().collect()
}

// 写入文件
fn write_file_contents(path: &Path, contents: &str) -> io::Result<()> {
    let mut file = File::create(path)?;
    file.write_all(contents.as_bytes())?;
    Ok(())
}

// 追加到文件
fn append_to_file(path: &Path, content: &str) -> io::Result<()> {
    let mut file = OpenOptions::new()
        .append(true)
        .create(true)  // 如果文件不存在则创建
        .open(path)?;
    
    writeln!(file, "{}", content)?;
    Ok(())
}

// 文件操作工具函数
fn demonstrate_file_operations() -> io::Result<()> {
    println!("=== 基本文件操作 ===");
    
    let test_file = Path::new("test.txt");
    
    // 写入测试文件
    write_file_contents(test_file, "Hello, World!\nThis is a test file.")?;
    println!("文件写入成功");
    
    // 读取整个文件
    let contents = read_file_contents(test_file)?;
    println!("文件内容:\n{}", contents);
    
    // 逐行读取
    let lines = read_file_lines(test_file)?;
    println!("文件行数: {}", lines.len());
    for (i, line) in lines.iter().enumerate() {
        println!("{}: {}", i + 1, line);
    }
    
    // 追加内容
    append_to_file(test_file, "This is appended content.")?;
    println!("内容追加成功");
    
    // 读取追加后的内容
    let updated_contents = read_file_contents(test_file)?;
    println!("更新后的内容:\n{}", updated_contents);
    
    // 清理测试文件
    std::fs::remove_file(test_file)?;
    println!("测试文件已清理");
    
    Ok(())
}

高级文件处理

对于大型文件或需要更复杂处理的情况,我们需要更高效的文件处理技术。

rust 复制代码
use std::io::{Error, ErrorKind};

// 处理大文件的缓冲读取
fn process_large_file<P, F>(path: P, mut processor: F) -> io::Result<()>
where
    P: AsRef<Path>,
    F: FnMut(&str) -> io::Result<()>,
{
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    
    for (line_num, line) in reader.lines().enumerate() {
        let line = line?;
        processor(&line)
            .map_err(|e| Error::new(ErrorKind::Other, format!("第 {} 行处理失败: {}", line_num + 1, e)))?;
    }
    
    Ok(())
}

// 二进制文件处理
fn read_binary_file(path: &Path) -> io::Result<Vec<u8>> {
    let mut file = File::open(path)?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;
    Ok(buffer)
}

fn write_binary_file(path: &Path, data: &[u8]) -> io::Result<()> {
    let mut file = File::create(path)?;
    file.write_all(data)?;
    Ok(())
}

// 文件信息查询
fn get_file_info(path: &Path) -> io::Result<()> {
    let metadata = path.metadata()?;
    
    println!("文件: {}", path.display());
    println!("大小: {} 字节", metadata.len());
    println!("类型: {}", if metadata.is_dir() { "目录" } else { "文件" });
    println!("权限: {}", if metadata.permissions().readonly() { "只读" } else { "可写" });
    
    if let Ok(modified) = metadata.modified() {
        println!("修改时间: {:?}", modified);
    }
    
    if let Ok(accessed) = metadata.accessed() {
        println!("访问时间: {:?}", accessed);
    }
    
    Ok(())
}

// 文件搜索功能
fn search_in_file(path: &Path, pattern: &str) -> io::Result<Vec<(usize, String)>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    
    let mut results = Vec::new();
    
    for (line_num, line) in reader.lines().enumerate() {
        let line = line?;
        if line.contains(pattern) {
            results.push((line_num + 1, line));
        }
    }
    
    Ok(results)
}

fn demonstrate_advanced_file_ops() -> io::Result<()> {
    println!("\n=== 高级文件操作 ===");
    
    let test_file = Path::new("advanced_test.txt");
    
    // 创建测试文件
    let test_content = "这是第一行\n这是第二行\n包含关键词的行\n这是第四行";
    write_file_contents(test_file, test_content)?;
    
    // 演示大文件处理模式
    println!("逐行处理文件:");
    process_large_file(test_file, |line| {
        println!("处理: {}", line);
        Ok(())
    })?;
    
    // 演示文件搜索
    println!("\n搜索包含'关键词'的行:");
    let results = search_in_file(test_file, "关键词")?;
    for (line_num, line) in results {
        println!("第 {} 行: {}", line_num, line);
    }
    
    // 演示二进制操作
    println!("\n二进制文件操作:");
    let binary_data = b"Hello, Binary World!";
    let binary_file = Path::new("binary_test.bin");
    write_binary_file(binary_file, binary_data)?;
    
    let read_data = read_binary_file(binary_file)?;
    println!("读取的二进制数据: {:?}", String::from_utf8_lossy(&read_data));
    
    // 文件信息
    println!("\n文件信息:");
    get_file_info(test_file)?;
    
    // 清理
    std::fs::remove_file(test_file)?;
    std::fs::remove_file(binary_file)?;
    
    Ok(())
}

健壮的错误处理

在 CLI 工具中,良好的错误处理对于用户体验至关重要。

rust 复制代码
use std::error::Error;
use std::fmt;
use std::process;

// 自定义错误类型
#[derive(Debug)]
enum CliError {
    Io(io::Error),
    Parse(String),
    Validation(String),
    Config(String),
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CliError::Io(e) => write!(f, "IO错误: {}", e),
            CliError::Parse(s) => write!(f, "解析错误: {}", s),
            CliError::Validation(s) => write!(f, "验证错误: {}", s),
            CliError::Config(s) => write!(f, "配置错误: {}", s),
        }
    }
}

impl Error for CliError {}

impl From<io::Error> for CliError {
    fn from(error: io::Error) -> Self {
        CliError::Io(error)
    }
}

// 文件处理结果类型
type CliResult<T> = Result<T, CliError>;

// 健壮的文件处理器
struct FileProcessor {
    input_path: PathBuf,
    output_path: Option<PathBuf>,
    verbose: bool,
}

impl FileProcessor {
    fn new(input_path: PathBuf, output_path: Option<PathBuf>, verbose: bool) -> Self {
        Self {
            input_path,
            output_path,
            verbose,
        }
    }
    
    fn validate(&self) -> CliResult<()> {
        if !self.input_path.exists() {
            return Err(CliError::Validation(format!("输入文件不存在: {}", self.input_path.display())));
        }
        
        if let Some(ref output) = self.output_path {
            if let Some(parent) = output.parent() {
                if !parent.exists() {
                    return Err(CliError::Validation(format!("输出目录不存在: {}", parent.display())));
                }
            }
        }
        
        Ok(())
    }
    
    fn process(&self) -> CliResult<()> {
        self.validate()?;
        
        if self.verbose {
            println!("处理文件: {}", self.input_path.display());
        }
        
        let content = read_file_contents(&self.input_path)
            .map_err(|e| CliError::Io(e))?;
        
        let processed_content = self.transform_content(&content)?;
        
        if let Some(ref output_path) = self.output_path {
            write_file_contents(output_path, &processed_content)
                .map_err(|e| CliError::Io(e))?;
            
            if self.verbose {
                println!("结果写入: {}", output_path.display());
            }
        } else {
            println!("处理后的内容:\n{}", processed_content);
        }
        
        Ok(())
    }
    
    fn transform_content(&self, content: &str) -> CliResult<String> {
        // 简单的转换:转换为大写
        Ok(content.to_uppercase())
    }
}

// 错误处理工具函数
fn handle_error(error: &dyn Error) {
    eprintln!("错误: {}", error);
    
    // 根据错误类型提供建议
    if let Some(cli_error) = error.downcast_ref::<CliError>() {
        match cli_error {
            CliError::Io(_) => {
                eprintln!("建议: 检查文件路径和权限");
            }
            CliError::Parse(_) => {
                eprintln!("建议: 检查输入格式");
            }
            CliError::Validation(_) => {
                eprintln!("建议: 验证输入参数");
            }
            CliError::Config(_) => {
                eprintln!("建议: 检查配置文件");
            }
        }
    }
    
    process::exit(1);
}

// 演示错误处理
fn demonstrate_error_handling() -> CliResult<()> {
    println!("=== 错误处理演示 ===");
    
    // 测试文件处理器的验证
    let processor = FileProcessor::new(
        PathBuf::from("nonexistent.txt"),
        Some(PathBuf::from("output.txt")),
        true,
    );
    
    match processor.validate() {
        Ok(()) => println!("验证通过"),
        Err(e) => {
            println!("验证失败: {}", e);
            // 在实际应用中,我们可能会在这里返回错误
        }
    }
    
    // 创建有效的测试文件
    let test_file = Path::new("test_input.txt");
    write_file_contents(test_file, "Hello, Error Handling!")?;
    
    // 成功的处理
    let good_processor = FileProcessor::new(
        test_file.to_path_buf(),
        Some(PathBuf::from("test_output.txt")),
        true,
    );
    
    good_processor.process()?;
    println!("文件处理成功完成");
    
    // 清理
    std::fs::remove_file(test_file)?;
    std::fs::remove_file("test_output.txt")?;
    
    Ok(())
}

fn main() {
    // 基本文件操作
    if let Err(e) = demonstrate_file_operations() {
        eprintln!("文件操作失败: {}", e);
    }
    
    // 高级文件操作
    if let Err(e) = demonstrate_advanced_file_ops() {
        eprintln!("高级文件操作失败: {}", e);
    }
    
    // 错误处理演示
    if let Err(e) = demonstrate_error_handling() {
        handle_error(&e);
    }
}

21.3 使用TDD模式开发库功能

测试驱动开发(TDD)是一种先写测试再实现功能的开发方法,它能帮助设计出更清晰、更可靠的API。

设置测试环境

首先,让我们设置一个支持TDD的项目结构。

Cargo.toml:

toml 复制代码
[package]
name = "cli-tool"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.0", features = ["derive"] }
anyhow = "1.0"
thiserror = "1.0"

[dev-dependencies]
tempfile = "3.3"

src/lib.rs - 核心库功能

rust 复制代码
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thiserror::Error;

// 自定义错误类型
#[derive(Error, Debug)]
pub enum CliToolError {
    #[error("IO错误: {0}")]
    Io(#[from] std::io::Error),
    #[error("解析错误: {0}")]
    Parse(String),
    #[error("配置错误: {0}")]
    Config(String),
    #[error("处理错误: {0}")]
    Processing(String),
}

// 配置结构体
#[derive(Debug, Clone)]
pub struct Config {
    pub input: PathBuf,
    pub output: Option<PathBuf>,
    pub verbose: bool,
    pub settings: HashMap<String, String>,
}

impl Config {
    pub fn new(input: PathBuf) -> Self {
        Self {
            input,
            output: None,
            verbose: false,
            settings: HashMap::new(),
        }
    }
    
    pub fn with_output(mut self, output: PathBuf) -> Self {
        self.output = Some(output);
        self
    }
    
    pub fn with_verbose(mut self, verbose: bool) -> Self {
        self.verbose = verbose;
        self
    }
    
    pub fn with_setting(mut self, key: &str, value: &str) -> Self {
        self.settings.insert(key.to_string(), value.to_string());
        self
    }
}

// 文本处理器 trait
pub trait TextProcessor {
    fn process(&self, text: &str) -> Result<String, CliToolError>;
}

// 简单的文本转换器
pub struct TextTransformer {
    pub operation: TransformOperation,
}

#[derive(Debug, Clone, PartialEq)]
pub enum TransformOperation {
    UpperCase,
    LowerCase,
    Reverse,
    WordCount,
}

impl TextProcessor for TextTransformer {
    fn process(&self, text: &str) -> Result<String, CliToolError> {
        match self.operation {
            TransformOperation::UpperCase => Ok(text.to_uppercase()),
            TransformOperation::LowerCase => Ok(text.to_lowercase()),
            TransformOperation::Reverse => Ok(text.chars().rev().collect()),
            TransformOperation::WordCount => Ok(text.split_whitespace().count().to_string()),
        }
    }
}

// 文件处理器
pub struct FileProcessor<P: TextProcessor> {
    config: Config,
    processor: P,
}

impl<P: TextProcessor> FileProcessor<P> {
    pub fn new(config: Config, processor: P) -> Self {
        Self { config, processor }
    }
    
    pub fn process(&self) -> Result<(), CliToolError> {
        self.validate()?;
        
        if self.config.verbose {
            println!("处理文件: {}", self.config.input.display());
        }
        
        let content = std::fs::read_to_string(&self.config.input)?;
        let processed_content = self.processor.process(&content)?;
        
        match &self.config.output {
            Some(output_path) => {
                std::fs::write(output_path, &processed_content)?;
                if self.config.verbose {
                    println!("结果写入: {}", output_path.display());
                }
            }
            None => {
                println!("{}", processed_content);
            }
        }
        
        Ok(())
    }
    
    fn validate(&self) -> Result<(), CliToolError> {
        if !self.config.input.exists() {
            return Err(CliToolError::Config(format!(
                "输入文件不存在: {}",
                self.config.input.display()
            )));
        }
        
        if let Some(ref output) = self.config.output {
            if let Some(parent) = output.parent() {
                if !parent.exists() {
                    return Err(CliToolError::Config(format!(
                        "输出目录不存在: {}",
                        parent.display()
                    )));
                }
            }
        }
        
        Ok(())
    }
}

编写测试

tests/text_processor_tests.rs

rust 复制代码
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_uppercase_transformation() {
        let processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        
        let result = processor.process("hello world").unwrap();
        assert_eq!(result, "HELLO WORLD");
    }
    
    #[test]
    fn test_lowercase_transformation() {
        let processor = TextTransformer {
            operation: TransformOperation::LowerCase,
        };
        
        let result = processor.process("HELLO WORLD").unwrap();
        assert_eq!(result, "hello world");
    }
    
    #[test]
    fn test_reverse_transformation() {
        let processor = TextTransformer {
            operation: TransformOperation::Reverse,
        };
        
        let result = processor.process("hello").unwrap();
        assert_eq!(result, "olleh");
    }
    
    #[test]
    fn test_word_count() {
        let processor = TextTransformer {
            operation: TransformOperation::WordCount,
        };
        
        let result = processor.process("hello world from rust").unwrap();
        assert_eq!(result, "4");
    }
    
    #[test]
    fn test_empty_input() {
        let processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        
        let result = processor.process("").unwrap();
        assert_eq!(result, "");
    }
}

tests/file_processor_tests.rs

rust 复制代码
#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::NamedTempFile;
    
    #[test]
    fn test_file_processor_validation() {
        let config = Config::new(PathBuf::from("nonexistent.txt"));
        let processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        let file_processor = FileProcessor::new(config, processor);
        
        let result = file_processor.validate();
        assert!(result.is_err());
    }
    
    #[test]
    fn test_file_processor_success() {
        // 创建临时输入文件
        let input_file = NamedTempFile::new().unwrap();
        fs::write(&input_file, "test content").unwrap();
        
        // 创建临时输出文件
        let output_file = NamedTempFile::new().unwrap();
        
        let config = Config::new(input_file.path().to_path_buf())
            .with_output(output_file.path().to_path_buf());
        
        let processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        
        let file_processor = FileProcessor::new(config, processor);
        let result = file_processor.process();
        
        assert!(result.is_ok());
        
        // 验证输出文件内容
        let output_content = fs::read_to_string(output_file.path()).unwrap();
        assert_eq!(output_content, "TEST CONTENT");
    }
    
    #[test]
    fn test_file_processor_stdout() {
        let input_file = NamedTempFile::new().unwrap();
        fs::write(&input_file, "test content").unwrap();
        
        let config = Config::new(input_file.path().to_path_buf());
        
        let processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        
        let file_processor = FileProcessor::new(config, processor);
        let result = file_processor.process();
        
        assert!(result.is_ok());
    }
}

实现功能

现在基于测试来实现和改进功能。

src/processor/mod.rs

rust 复制代码
use crate::{CliToolError, TextProcessor};
use std::collections::HashMap;

// 更复杂的文本处理器:单词频率统计
pub struct WordFrequencyProcessor;

impl TextProcessor for WordFrequencyProcessor {
    fn process(&self, text: &str) -> Result<String, CliToolError> {
        let words: Vec<&str> = text
            .split_whitespace()
            .map(|word| {
                // 清理单词:移除标点符号,转换为小写
                word.trim_matches(|c: char| !c.is_alphabetic())
                    .to_lowercase()
            })
            .filter(|word| !word.is_empty())
            .map(|word| word.into_boxed_str())
            .collect::<Vec<_>>()
            .into_iter()
            .map(|s| s.into_string())
            .collect::<Vec<String>>()
            .iter()
            .map(|s| s.as_str())
            .collect();
        
        let mut frequency: HashMap<&str, usize> = HashMap::new();
        
        for &word in &words {
            *frequency.entry(word).or_insert(0) += 1;
        }
        
        // 按频率排序
        let mut sorted_freq: Vec<(&str, usize)> = frequency.into_iter().collect();
        sorted_freq.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
        
        // 格式化输出
        let result = sorted_freq
            .into_iter()
            .map(|(word, count)| format!("{}: {}", word, count))
            .collect::<Vec<String>>()
            .join("\n");
        
        Ok(result)
    }
}

// 文本过滤器
pub struct TextFilter {
    pub pattern: String,
    pub case_sensitive: bool,
}

impl TextProcessor for TextFilter {
    fn process(&self, text: &str) -> Result<String, CliToolError> {
        let lines: Vec<&str> = text.lines().collect();
        let filtered_lines: Vec<&str> = if self.case_sensitive {
            lines
                .into_iter()
                .filter(|line| line.contains(&self.pattern))
                .collect()
        } else {
            let pattern_lower = self.pattern.to_lowercase();
            lines
                .into_iter()
                .filter(|line| line.to_lowercase().contains(&pattern_lower))
                .collect()
        };
        
        Ok(filtered_lines.join("\n"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_word_frequency() {
        let processor = WordFrequencyProcessor;
        let text = "hello world hello rust world hello";
        let result = processor.process(text).unwrap();
        
        assert!(result.contains("hello: 3"));
        assert!(result.contains("world: 2"));
        assert!(result.contains("rust: 1"));
    }
    
    #[test]
    fn test_text_filter_case_sensitive() {
        let processor = TextFilter {
            pattern: "Hello".to_string(),
            case_sensitive: true,
        };
        
        let text = "Hello World\nhello world\nHELLO WORLD";
        let result = processor.process(text).unwrap();
        
        assert_eq!(result, "Hello World");
    }
    
    #[test]
    fn test_text_filter_case_insensitive() {
        let processor = TextFilter {
            pattern: "hello".to_string(),
            case_sensitive: false,
        };
        
        let text = "Hello World\nhello world\nHELLO WORLD\nGoodbye";
        let result = processor.process(text).unwrap();
        
        let lines: Vec<&str> = result.lines().collect();
        assert_eq!(lines.len(), 3);
        assert!(lines.contains(&"Hello World"));
        assert!(lines.contains(&"hello world"));
        assert!(lines.contains(&"HELLO WORLD"));
    }
}

集成测试

tests/integration_tests.rs

rust 复制代码
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation};
use std::fs;
use tempfile::NamedTempFile;

#[test]
fn test_end_to_end_uppercase() {
    // 设置输入文件
    let input_file = NamedTempFile::new().unwrap();
    fs::write(&input_file, "Hello Integration Test").unwrap();
    
    // 设置输出文件
    let output_file = NamedTempFile::new().unwrap();
    
    // 创建配置和处理器
    let config = Config::new(input_file.path().to_path_buf())
        .with_output(output_file.path().to_path_buf())
        .with_verbose(false);
    
    let text_processor = TextTransformer {
        operation: TransformOperation::UpperCase,
    };
    
    let file_processor = FileProcessor::new(config, text_processor);
    
    // 执行处理
    let result = file_processor.process();
    assert!(result.is_ok());
    
    // 验证结果
    let output_content = fs::read_to_string(output_file.path()).unwrap();
    assert_eq!(output_content, "HELLO INTEGRATION TEST");
}

#[test]
fn test_end_to_end_multiple_operations() {
    let input_file = NamedTempFile::new().unwrap();
    fs::write(&input_file, "First line\nSecond line\nThird line").unwrap();
    
    let config = Config::new(input_file.path().to_path_buf())
        .with_verbose(false);
    
    // 测试多个操作
    let operations = vec![
        TransformOperation::UpperCase,
        TransformOperation::Reverse,
        TransformOperation::WordCount,
    ];
    
    for operation in operations {
        let text_processor = TextTransformer { operation };
        let file_processor = FileProcessor::new(config.clone(), text_processor);
        
        let result = file_processor.process();
        assert!(result.is_ok(), "操作 {:?} 失败", operation);
    }
}

21.4 编写完整的生产级工具

现在让我们将所有部分组合起来,构建一个完整的生产级命令行工具。

完整的CLI应用

src/main.rs

rust 复制代码
use clap::{Parser, Subcommand, ValueEnum};
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation, TextFilter, WordFrequencyProcessor};
use std::path::PathBuf;
use anyhow::{Result, Context};

#[derive(Parser)]
#[command(name = "textool", version = "1.0", about = "强大的文本处理工具", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 文本转换操作
    Transform {
        /// 输入文件路径
        #[arg(short, long)]
        input: PathBuf,
        
        /// 输出文件路径
        #[arg(short, long)]
        output: Option<PathBuf>,
        
        /// 转换操作类型
        #[arg(value_enum)]
        operation: TransformOp,
        
        /// 启用详细输出
        #[arg(short, long)]
        verbose: bool,
    },
    
    /// 文本过滤操作
    Filter {
        /// 输入文件路径
        #[arg(short, long)]
        input: PathBuf,
        
        /// 输出文件路径
        #[arg(short, long)]
        output: Option<PathBuf>,
        
        /// 过滤模式
        #[arg(short, long)]
        pattern: String,
        
        /// 是否区分大小写
        #[arg(short = 'i', long)]
        case_sensitive: bool,
        
        /// 启用详细输出
        #[arg(short, long)]
        verbose: bool,
    },
    
    /// 单词频率统计
    Frequency {
        /// 输入文件路径
        #[arg(short, long)]
        input: PathBuf,
        
        /// 输出文件路径
        #[arg(short, long)]
        output: Option<PathBuf>,
        
        /// 启用详细输出
        #[arg(short, long)]
        verbose: bool,
    },
}

#[derive(Clone, ValueEnum)]
enum TransformOp {
    Upper,
    Lower,
    Reverse,
    Count,
}

impl From<TransformOp> for TransformOperation {
    fn from(op: TransformOp) -> Self {
        match op {
            TransformOp::Upper => TransformOperation::UpperCase,
            TransformOp::Lower => TransformOperation::LowerCase,
            TransformOp::Reverse => TransformOperation::Reverse,
            TransformOp::Count => TransformOperation::WordCount,
        }
    }
}

// 应用程序主结构体
struct TextoolApp;

impl TextoolApp {
    fn run() -> Result<()> {
        let cli = Cli::parse();
        
        match cli.command {
            Commands::Transform { input, output, operation, verbose } => {
                self::handle_transform(input, output, operation, verbose)
            }
            Commands::Filter { input, output, pattern, case_sensitive, verbose } => {
                self::handle_filter(input, output, pattern, case_sensitive, verbose)
            }
            Commands::Frequency { input, output, verbose } => {
                self::handle_frequency(input, output, verbose)
            }
        }
    }
}

fn handle_transform(
    input: PathBuf,
    output: Option<PathBuf>,
    operation: TransformOp,
    verbose: bool,
) -> Result<()> {
    let config = Config::new(input)
        .with_output_opt(output)
        .with_verbose(verbose);
    
    let processor = TextTransformer {
        operation: operation.into(),
    };
    
    let file_processor = FileProcessor::new(config, processor);
    file_processor.process()
        .context("文本转换操作失败")
}

fn handle_filter(
    input: PathBuf,
    output: Option<PathBuf>,
    pattern: String,
    case_sensitive: bool,
    verbose: bool,
) -> Result<()> {
    let config = Config::new(input)
        .with_output_opt(output)
        .with_verbose(verbose);
    
    let processor = TextFilter {
        pattern,
        case_sensitive,
    };
    
    let file_processor = FileProcessor::new(config, processor);
    file_processor.process()
        .context("文本过滤操作失败")
}

fn handle_frequency(
    input: PathBuf,
    output: Option<PathBuf>,
    verbose: bool,
) -> Result<()> {
    let config = Config::new(input)
        .with_output_opt(output)
        .with_verbose(verbose);
    
    let processor = WordFrequencyProcessor;
    
    let file_processor = FileProcessor::new(config, processor);
    file_processor.process()
        .context("单词频率统计失败")
}

// 为Config添加辅助方法
trait ConfigExt {
    fn with_output_opt(self, output: Option<PathBuf>) -> Self;
}

impl ConfigExt for Config {
    fn with_output_opt(mut self, output: Option<PathBuf>) -> Self {
        self.output = output;
        self
    }
}

fn main() {
    if let Err(e) = TextoolApp::run() {
        eprintln!("错误: {:?}", e);
        
        // 提供用户友好的错误信息
        if let Some(source) = e.source() {
            eprintln!("原因: {}", source);
        }
        
        std::process::exit(1);
    }
}

配置管理和环境变量

src/config.rs

rust 复制代码
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::CliToolError;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
    pub default_input: Option<PathBuf>,
    pub default_output_dir: Option<PathBuf>,
    pub settings: HashMap<String, String>,
}

impl Default for AppConfig {
    fn default() -> Self {
        let mut settings = HashMap::new();
        settings.insert("encoding".to_string(), "utf-8".to_string());
        settings.insert("max_file_size".to_string(), "1048576".to_string()); // 1MB
        
        Self {
            default_input: None,
            default_output_dir: Some(PathBuf::from(".")),
            settings,
        }
    }
}

impl AppConfig {
    pub fn load() -> Result<Self, CliToolError> {
        let config_paths = vec![
            Path::new("textool.toml"),
            Path::new("~/.config/textool/config.toml"),
            Path::new("/etc/textool/config.toml"),
        ];
        
        for path in config_paths {
            if path.exists() {
                let content = fs::read_to_string(path)
                    .map_err(|e| CliToolError::Config(format!("无法读取配置文件 {}: {}", path.display(), e)))?;
                
                return toml::from_str(&content)
                    .map_err(|e| CliToolError::Config(format!("配置文件解析错误: {}", e)));
            }
        }
        
        // 没有找到配置文件,返回默认配置
        Ok(Self::default())
    }
    
    pub fn save(&self, path: &Path) -> Result<(), CliToolError> {
        let content = toml::to_string_pretty(self)
            .map_err(|e| CliToolError::Config(format!("配置序列化错误: {}", e)))?;
        
        fs::write(path, content)
            .map_err(|e| CliToolError::Config(format!("无法保存配置文件: {}", e)))?;
        
        Ok(())
    }
    
    pub fn get_setting(&self, key: &str) -> Option<&String> {
        self.settings.get(key)
    }
    
    pub fn set_setting(&mut self, key: &str, value: &str) {
        self.settings.insert(key.to_string(), value.to_string());
    }
}

日志和监控

src/logging.rs

rust 复制代码
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};

#[derive(Clone)]
pub struct Logger {
    inner: Arc<Mutex<LoggerInner>>,
}

struct LoggerInner {
    file: Option<File>,
    verbose: bool,
}

impl Logger {
    pub fn new(verbose: bool) -> Self {
        Self {
            inner: Arc::new(Mutex::new(LoggerInner {
                file: None,
                verbose,
            })),
        }
    }
    
    pub fn with_file<P: AsRef<Path>>(self, path: P, verbose: bool) -> io::Result<Self> {
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;
            
        let mut inner = self.inner.lock().unwrap();
        inner.file = Some(file);
        inner.verbose = verbose;
        
        Ok(self)
    }
    
    pub fn info(&self, message: &str) {
        self.log("INFO", message);
    }
    
    pub fn warn(&self, message: &str) {
        self.log("WARN", message);
    }
    
    pub fn error(&self, message: &str) {
        self.log("ERROR", message);
    }
    
    pub fn debug(&self, message: &str) {
        let inner = self.inner.lock().unwrap();
        if inner.verbose {
            self.log("DEBUG", message);
        }
    }
    
    fn log(&self, level: &str, message: &str) {
        let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
        let log_message = format!("[{}] {}: {}\n", timestamp, level, message);
        
        let mut inner = self.inner.lock().unwrap();
        
        // 输出到标准错误
        eprint!("{}", log_message);
        
        // 写入日志文件
        if let Some(ref mut file) = inner.file {
            let _ = file.write_all(log_message.as_bytes());
        }
    }
}

impl fmt::Debug for Logger {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let inner = self.inner.lock().unwrap();
        write!(f, "Logger(verbose: {}, has_file: {})", 
               inner.verbose, inner.file.is_some())
    }
}

性能优化和高级特性

src/optimized_processor.rs

rust 复制代码
use crate::{CliToolError, TextProcessor};
use std::collections::HashMap;
use rayon::prelude::*;

// 使用Rayon进行并行处理
pub struct ParallelTextProcessor<P: TextProcessor + Send + Sync> {
    chunk_size: usize,
    processor: P,
}

impl<P: TextProcessor + Send + Sync> ParallelTextProcessor<P> {
    pub fn new(processor: P, chunk_size: usize) -> Self {
        Self { processor, chunk_size }
    }
}

impl<P: TextProcessor + Send + Sync> TextProcessor for ParallelTextProcessor<P> {
    fn process(&self, text: &str) -> Result<String, CliToolError> {
        let lines: Vec<&str> = text.lines().collect();
        
        if lines.len() <= self.chunk_size {
            // 对于小文件,使用串行处理
            return self.processor.process(text);
        }
        
        // 并行处理行
        let processed_lines: Result<Vec<String>, CliToolError> = lines
            .par_chunks(self.chunk_size)
            .map(|chunk| {
                let chunk_text = chunk.join("\n");
                self.processor.process(&chunk_text)
            })
            .collect();
        
        let processed_chunks = processed_lines?;
        Ok(processed_chunks.join("\n"))
    }
}

// 流式处理器,用于处理大文件
pub struct StreamingProcessor {
    buffer_size: usize,
}

impl StreamingProcessor {
    pub fn new(buffer_size: usize) -> Self {
        Self { buffer_size }
    }
}

impl TextProcessor for StreamingProcessor {
    fn process(&self, text: &str) -> Result<String, CliToolError> {
        // 对于流式处理,我们可能想要不同的接口
        // 这里简化实现,只是用缓冲方式处理
        let mut result = String::with_capacity(text.len());
        let mut words = text.split_whitespace();
        
        let mut buffer = Vec::with_capacity(self.buffer_size);
        
        while let Some(word) = words.next() {
            buffer.push(word.to_uppercase());
            
            if buffer.len() >= self.buffer_size {
                result.push_str(&buffer.join(" "));
                result.push(' ');
                buffer.clear();
            }
        }
        
        // 处理剩余内容
        if !buffer.is_empty() {
            result.push_str(&buffer.join(" "));
        }
        
        Ok(result)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{TextTransformer, TransformOperation};
    
    #[test]
    fn test_parallel_processor() {
        let base_processor = TextTransformer {
            operation: TransformOperation::UpperCase,
        };
        
        let parallel_processor = ParallelTextProcessor::new(base_processor, 2);
        
        let text = "line one\nline two\nline three\nline four";
        let result = parallel_processor.process(text).unwrap();
        
        assert!(result.contains("LINE ONE"));
        assert!(result.contains("LINE TWO"));
        assert!(result.contains("LINE THREE"));
        assert!(result.contains("LINE FOUR"));
    }
    
    #[test]
    fn test_streaming_processor() {
        let processor = StreamingProcessor::new(3);
        let text = "this is a test of streaming processing";
        let result = processor.process(text).unwrap();
        
        // 验证所有单词都被转换为大写
        assert!(!result.contains("this"));
        assert!(result.contains("THIS"));
    }
}

完整的生产级工具使用示例

让我们创建一个完整的使用示例,展示工具的所有功能。

examples/advanced_usage.rs

rust 复制代码
use cli_tool::{Config, FileProcessor, TextTransformer, TransformOperation, TextFilter, WordFrequencyProcessor};
use std::path::PathBuf;

fn main() -> anyhow::Result<()> {
    println!("=== 文本工具高级使用示例 ===\n");
    
    // 示例1: 基本文本转换
    println!("1. 基本文本转换示例");
    let config = Config::new(PathBuf::from("examples/sample.txt"))
        .with_verbose(true);
    
    let processor = TextTransformer {
        operation: TransformOperation::UpperCase,
    };
    
    let file_processor = FileProcessor::new(config, processor);
    
    // 在实际使用中,我们会处理真实文件
    // 这里只是演示API使用
    println!("配置: {:?}", file_processor);
    println!("---\n");
    
    // 示例2: 文本过滤
    println!("2. 文本过滤示例");
    let filter_config = Config::new(PathBuf::from("examples/sample.txt"))
        .with_output(PathBuf::from("examples/filtered.txt"))
        .with_verbose(true);
    
    let filter_processor = TextFilter {
        pattern: "important".to_string(),
        case_sensitive: false,
    };
    
    let filter_file_processor = FileProcessor::new(filter_config, filter_processor);
    println!("过滤器配置: {:?}", filter_file_processor);
    println!("---\n");
    
    // 示例3: 单词频率统计
    println!("3. 单词频率统计示例");
    let freq_config = Config::new(PathBuf::from("examples/sample.txt"))
        .with_verbose(true);
    
    let freq_processor = WordFrequencyProcessor;
    let freq_file_processor = FileProcessor::new(freq_config, freq_processor);
    println!("频率分析器: {:?}", freq_file_processor);
    
    println!("\n=== 示例完成 ===");
    
    Ok(())
}

总结

本章详细介绍了如何使用Rust构建功能完整的命令行工具:

  1. 命令行参数解析:使用标准库和clap库处理各种参数格式
  2. 文件操作和错误处理:健壮的文件读写和全面的错误处理策略
  3. 测试驱动开发:通过TDD模式开发可靠的核心库功能
  4. 生产级工具构建:集成配置管理、日志记录、性能优化等高级特性

通过本章的学习,你应该能够:

  • 使用clap构建复杂的命令行界面
  • 实现健壮的文件处理和错误处理
  • 使用TDD方法开发可靠的库功能
  • 构建包含配置管理、日志记录的生产级工具
  • 优化工具性能,处理大文件和并行处理

这些技能不仅适用于构建文本处理工具,也可以应用于各种类型的命令行应用程序开发。Rust的性能优势和安全性使其成为构建命令行工具的绝佳选择。

相关推荐
极光代码工作室1 小时前
基于SpringBoot的校园招聘信息管理系统的设计与实现
java·前端·spring
蒋星熠1 小时前
常见反爬策略与破解反爬方法:爬虫工程师的攻防实战指南
开发语言·人工智能·爬虫·python·网络安全·网络爬虫
未若君雅裁1 小时前
斐波那契数列 - 动态规划实现 详解笔记
java·数据结构·笔记·算法·动态规划·代理模式
断剑zou天涯1 小时前
【算法笔记】从暴力递归到动态规划(三)
java·算法·动态规划
断剑zou天涯1 小时前
【算法笔记】从暴力递归到动态规划(一)
java·算法·动态规划
飞梦工作室1 小时前
突破 pandas 瓶颈:实时读写 Excel 与超透视汇总函数的双维解决方案
python·excel·pandas
Ace_31750887761 小时前
微店平台关键字搜索接口深度解析:从 Token 动态生成到多维度数据挖掘
java·前端·javascript
yyt3630458411 小时前
Maven 命令构建成功但 IDEA 构建失败原因解析
java·maven·intellij-idea
krafft1 小时前
从零入门 Spring AI,详细拆解 ChatClient 调用流程和 Advisor 底层原理,小白可入!
java·spring·ai