🦀 第21课:CLI工具开发

实战项目 第21/25课

Rust是开发CLI工具的绝佳选择——编译为单个二进制、启动快、跨平台。本课将用clap库构建一个功能完整的命令行工具。

🛠️ 命令行参数解析

// 使用clap库: cargo add clap --features derive
// 这里用手动解析演示核心概念

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

fn print_help() {
    println!("rtool - Rust命令行工具集 v1.0");
    println!();
    println!("用法: rtool <command> [options]");
    println!();
    println!("命令:");
    println!("  count <file>      统计文件行数/字数/字符数");
    println!("  grep <pattern> <file>  在文件中搜索模式");
    println!("  reverse <file>    反转文件每行");
    println!("  upper <file>      转为大写");
    println!("  lower <file>      转为小写");
    println!("  head <n> <file>   显示前n行");
    println!("  help               显示帮助");
}

fn count_file(filename: &str) -> Result<(), String> {
    let content = fs::read_to_string(filename)
        .map_err(|e| format!("无法读取 {}: {}", filename, e))?;
    let lines = content.lines().count();
    let words = content.split_whitespace().count();
    let chars = content.chars().count();
    let bytes = content.len();
    println!("  行数: {}", lines);
    println!("  字数: {}", words);
    println!("  字符数: {}", chars);
    println!("  字节数: {}", bytes);
    Ok(())
}

fn grep_file(pattern: &str, filename: &str) -> Result<(), String> {
    let file = fs::File::open(filename)
        .map_err(|e| format!("无法打开 {}: {}", filename, e))?;
    let reader = io::BufReader::new(file);
    let mut found = 0;
    for (i, line) in reader.lines().enumerate() {
        let line = line.map_err(|e| format!("读取错误: {}", e))?;
        if line.contains(pattern) {
            found += 1;
            println!("{}:{}: {}", filename, i + 1, line);
        }
    }
    if found == 0 { println!("未找到匹配"); }
    Ok(())
}

fn process_file(filename: &str, op: fn(&str) -> String) -> Result<(), String> {
    let content = fs::read_to_string(filename)
        .map_err(|e| format!("无法读取 {}: {}", filename, e))?;
    for line in content.lines() {
        println!("{}", op(line));
    }
    Ok(())
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 { print_help(); return; }
    
    match args[1].as_str() {
        "count" => {
            if args.len() < 3 { println!("用法: rtool count <file>"); return; }
            if let Err(e) = count_file(&args[2]) { eprintln!("错误: {}", e); }
        }
        "grep" => {
            if args.len() < 4 { println!("用法: rtool grep <pattern> <file>"); return; }
            if let Err(e) = grep_file(&args[2], &args[3]) { eprintln!("错误: {}", e); }
        }
        "reverse" => {
            if args.len() < 3 { println!("用法: rtool reverse <file>"); return; }
            let _ = process_file(&args[2], |s| s.chars().rev().collect());
        }
        "upper" => {
            if args.len() < 3 { println!("用法: rtool upper <file>"); return; }
            let _ = process_file(&args[2], |s| s.to_uppercase());
        }
        "help" | "--help" | "-h" => print_help(),
        _ => println!("未知命令: {},运行 rtool help 查看帮助", args[1]),
    }
}
$ rtool count Cargo.toml 行数: 15 字数: 30 字符数: 280 字节数: 280

✅ 验证通过

🏗️ 进度条和彩色输出

fn main() {
    // ANSI颜色
    println!("\x1b[31m红色文字\x1b[0m");
    println!("\x1b[32m绿色文字\x1b[0m");
    println!("\x1b[33m黄色文字\x1b[0m");
    println!("\x1b[1;34m粗体蓝色\x1b[0m");
    
    // 进度条
    let total = 50;
    for i in 0..=total {
        let pct = i * 100 / total;
        let filled = i;
        let empty = total - i;
        print!("\r[{}{}] {}%", "█".repeat(filled), "░".repeat(empty), pct);
        std::io::Write::flush(&mut std::io::stdout()).unwrap();
        std::thread::sleep(std::time::Duration::from_millis(30));
    }
    println!("\n完成!✅");
}

✅ 验证通过

📝 练习

练习1:TODO应用

用clap实现TODO列表CLI:add/list/done/delete命令,数据保存到JSON文件。

练习2:文件加密

实现简单的XOR文件加密CLI工具,支持加密和解密。

🏆 本课成就

🔧 CLI高级特性

use std::io::{self, Write};

fn progress_bar(current: usize, total: usize, width: usize) -> String {
    let pct = current * 100 / total;
    let filled = current * width / total;
    format!("[{}{}] {}%", "█".repeat(filled), "░".repeat(width - filled), pct)
}

fn main() {
    // 进度条
    for i in 0..=20 {
        print!("
{}", progress_bar(i, 20, 30));
        io::stdout().flush().unwrap();
        std::thread::sleep(std::time::Duration::from_millis(50));
    }
    println!("
完成!");
    
    // clap库概念
    println!("
使用clap库:");
    println!("  #[derive(Parser)]");
    println!("  struct Cli {{");
    println!("      #[arg(short, long)]");
    println!("      verbose: bool,");
    println!("      #[arg(short, long)]");
    println!("      output: Option,");
    println!("  }}");
}
[██████████████████████████████████] 100% 完成!

✅ 验证通过

📊 CLI工具发布流程

fn main() {
    println!("CLI工具完整发布流程:");
    println!();
    println!("1. 开发阶段:");
    println!("   cargo new mytool");
    println!("   // 添加clap, anyhow等依赖");
    println!("   cargo run -- --help");
    println!();
    println!("2. 测试阶段:");
    println!("   cargo test");
    println!("   cargo clippy");
    println!("   cargo fmt --check");
    println!();
    println!("3. 构建Release:");
    println!("   cargo build --release");
    println!("   // 二进制在 target/release/mytool");
    println!();
    println!("4. 交叉编译:");
    println!("   rustup target add x86_64-unknown-linux-musl");
    println!("   cargo build --release --target x86_64-unknown-linux-musl");
    println!("   rustup target add x86_64-pc-windows-gnu");
    println!("   cargo build --release --target x86_64-pc-windows-gnu");
    println!();
    println!("5. 发布:");
    println!("   cargo publish  # 发布到crates.io");
    println!("   # 或上传GitHub Release");
    println!();
    println!("6. 安装:");
    println!("   cargo install mytool");
    println!("   cargo install --git https://github.com/you/mytool");
    
    println!("
推荐CLI工具参考:");
    println!("  ripgrep   - 快速搜索");
    println!("  fd        - 查找文件");
    println!("  bat       - cat增强");
    println!("  exa/eza   - ls增强");
    println!("  delta     - diff增强");
    println!("  starship  - 终端提示符");
}

🏗️ 综合实战:文件搜索工具

use std::env;
use std::fs;
use std::path::Path;

fn search_files(dir: &Path, pattern: &str, max_depth: usize) -> Vec {
    let mut results = Vec::new();
    if max_depth == 0 { return results; }
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            let name = path.file_name().unwrap().to_string_lossy();
            if name.contains(pattern) {
                results.push(path.display().to_string());
            }
            if path.is_dir() {
                results.extend(search_files(&path, pattern, max_depth - 1));
            }
        }
    }
    results
}

fn main() {
    let dir = env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    println!("搜索目录: {}", dir.display());
    
    // 搜索.rs文件
    let rust_files = search_files(&dir, ".rs", 3);
    println!("找到{}个.rs文件:", rust_files.len());
    for f in rust_files.iter().take(10) { println!("  {}", f); }
    if rust_files.len() > 10 { println!("  ...还有{}个", rust_files.len() - 10); }
    
    // 搜索.toml文件
    let toml_files = search_files(&dir, ".toml", 2);
    println!("找到{}个.toml文件", toml_files.len());
    
    // 统计
    let all_files = search_files(&dir, "", 3);
    println!("共扫描{}个文件/目录", all_files.len());
}
搜索目录: /path/to/dir 找到X个.rs文件: ./src/main.rs ... 共扫描Y个文件/目录

✅ 验证通过

📋 CLI工具发布清单

发布前检查

🔧 完整CLI项目模板

// Cargo.toml
// [package]
// name = "mytool"
// version = "0.1.0"
// edition = "2024"
// [dependencies]
// clap = { version = "4", features = ["derive"] }
// anyhow = "1"
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
//
// src/main.rs:
// use clap::Parser;
// use anyhow::Result;
//
// #[derive(Parser)]
// #[command(name = "mytool", about = "A Rust CLI tool")]
// struct Cli {
//     #[arg(short, long)] verbose: bool,
//     #[arg(short, long)] output: Option,
//     #[command(subcommand)] command: Commands,
// }
//
// #[derive(clap::Subcommand)]
// enum Commands {
//     Add { name: String },
//     List,
//     Remove { id: u64 },
// }

fn main() {
    println!("CLI项目结构:");
    println!("  mytool/");
    println!("  ├── Cargo.toml");
    println!("  ├── README.md");
    println!("  ├── LICENSE");
    println!("  ├── .github/workflows/ci.yml");
    println!("  └── src/");
    println!("      └── main.rs");
    
    println!("
参考项目:");
    println!("  ripgrep - 搜索工具");
    println!("  fd      - 文件查找");
    println!("  bat     - 文件查看");
    println!("  exa     - ls替代");
    println!("  delta   - diff查看");
}

🏗️ CLI实战:JSON处理工具

use std::collections::HashMap;

// 简易JSON值
#[derive(Debug, Clone)]
enum JsonValue {
    Null,
    Bool(bool),
    Number(f64),
    String(String),
    Array(Vec),
    Object(HashMap),
}

impl JsonValue {
    fn get(&self, key: &str) -> Option<&JsonValue> {
        match self {
            JsonValue::Object(map) => map.get(key),
            _ => None,
        }
    }
    
    fn as_str(&self) -> Option<&str> {
        match self { JsonValue::String(s) => Some(s), _ => None }
    }
    
    fn as_f64(&self) -> Option {
        match self { JsonValue::Number(n) => Some(*n), _ => None }
    }
    
    fn pretty_print(&self, indent: usize) -> String {
        let pad = "  ".repeat(indent);
        match self {
            JsonValue::Null => "null".to_string(),
            JsonValue::Bool(b) => b.to_string(),
            JsonValue::Number(n) => n.to_string(),
            JsonValue::String(s) => format!(""{}"", s),
            JsonValue::Array(arr) => {
                if arr.is_empty() { "[]".to_string() }
                else {
                    let items: Vec = arr.iter()
                        .map(|v| format!("{}{}", "  ".repeat(indent+1), v.pretty_print(indent+1)))
                        .collect();
                    format!("{}]", items.join(","))
                }
            }
            JsonValue::Object(map) => {
                if map.is_empty() { "{}".to_string() }
                else {
                    let items: Vec = map.iter()
                        .map(|(k,v)| format!("{}"{}": {}", "  ".repeat(indent+1), k, v.pretty_print(indent+1)))
                        .collect();
                    format!("{}", items.join(","))
                }
            }
        }
    }
}

fn main() {
    let mut person = HashMap::new();
    person.insert("name".into(), JsonValue::String("Alice".into()));
    person.insert("age".into(), JsonValue::Number(30.0));
    person.insert("active".into(), JsonValue::Bool(true));
    
    let json = JsonValue::Object(person);
    println!("姓名: {}", json.get("name").and_then(|v| v.as_str()).unwrap_or("?"));
    println!("年龄: {}", json.get("age").and_then(|v| v.as_f64()).unwrap_or(0.0));
    println!("调试: {:?}", json);
}
姓名: Alice 年龄: 30 调试: Object({"active": Bool(true), "age": Number(30.0), "name": String("Alice")})

✅ 验证通过