第21章 構建命令行工具_#計算機視覺

文章目錄

  • 第21章 構建命令行工具
  • 21.1 接受命令行參數
  • 使用標準庫處理參數
  • 使用 clap 庫進行高級參數解析
  • 參數驗證和轉換
  • 21.2 讀取文件和錯誤處理
  • 基本文件操作
  • 高級文件處理
  • 健壯的錯誤處理
  • 21.3 使用TDD模式開發庫功能
  • 設置測試環境
  • 編寫測試
  • 實現功能
  • 集成測試
  • 21.4 編寫完整的生產級工具
  • 完整的CLI應用
  • 配置管理和環境變量
  • 日誌和監控
  • 性能優化和高級特性
  • 完整的生產級工具使用示例
  • 總結

第21章 構建命令行工具

命令行工具是系統編程和日常開發中不可或缺的一部分。Rust憑藉其出色的性能、內存安全性和強大的生態系統,成為了構建命令行工具的絕佳選擇。本章將全面介紹如何使用Rust構建功能完整、用户友好的命令行應用程序。

21.1 接受命令行參數

命令行參數是用户與工具交互的主要方式。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 中添加依賴:

[dependencies]
clap = { version = "4.0", features = ["derive"] }
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 工具的重要環節。

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 的所有權系統和錯誤處理機制讓這些操作既安全又高效。

基本文件操作

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(())
}

高級文件處理

對於大型文件或需要更復雜處理的情況,我們需要更高效的文件處理技術。

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 工具中,良好的錯誤處理對於用户體驗至關重要。

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:

[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 - 核心庫功能

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

#[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

#[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

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

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

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

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

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

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

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的性能優勢和安全性使其成為構建命令行工具的絕佳選擇。