目标

构建一个命令行文件搜索工具 rf (Rust Find),功能:

  • 按文件名模式搜索 (支持正则)
  • 显示匹配行的上下文
  • 实时进度条
  • 彩色输出
  • 单文件可执行,无需运行时

项目初始化

cargo new rf
cd rf
cargo add clap --features derive      # CLI 参数解析
cargo add regex                        # 正则匹配
cargo add indicatif                    # 进度条
cargo add colored                      # 彩色输出
cargo add walkdir                      # 递归遍历目录
cargo add anyhow                       # 错误处理

数据结构

// src/main.rs
use clap::Parser;
use regex::Regex;
use indicatif::{ProgressBar, ProgressStyle};
use colored::*;
use walkdir::WalkDir;
use anyhow::{Result, Context};
use std::path::PathBuf;

/// 文件搜索工具 — 按模式匹配文件内容
#[derive(Parser, Debug)]
#[command(name = "rf", version, about)]
struct Args {
    /// 搜索模式 (支持正则)
    pattern: String,

    /// 搜索目录 (默认当前目录)
    #[arg(default_value = ".")]
    directory: String,

    /// 显示匹配行前后行数
    #[arg(short = 'C', long, default_value = "2")]
    context: usize,

    /// 忽略大小写
    #[arg(short = 'i', long)]
    ignore_case: bool,

    /// 仅显示文件名
    #[arg(short = 'l', long)]
    files_only: bool,
}

核心搜索逻辑

struct Match {
    file: PathBuf,
    line_number: usize,
    line: String,
}

fn search_file(
    path: &PathBuf,
    regex: &Regex,
    args: &Args,
) -> Result<Vec<Match>> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("无法读取: {}", path.display()))?;

    let lines: Vec<&str> = content.lines().collect();
    let mut matches = Vec::new();

    for (i, line) in lines.iter().enumerate() {
        if regex.is_match(line) {
            matches.push(Match {
                file: path.clone(),
                line_number: i + 1,
                line: line.to_string(),
            });
        }
    }

    Ok(matches)
}

fn print_match(m: &Match, regex: &Regex, args: &Args) {
    if args.files_only {
        println!("{}", m.file.display().to_string().green());
        return;
    }

    let highlighted = regex.replace_all(&m.line, |caps: &regex::Captures| {
        caps[0].red().bold().to_string()
    });

    println!(
        "{}:{}  {}",
        m.file.display().to_string().green(),
        m.line_number.to_string().cyan(),
        highlighted
    );
}

进度条集成

fn main() -> Result<()> {
    let args = Args::parse();

    let regex = if args.ignore_case {
        Regex::new(&format!("(?i){}", args.pattern))?
    } else {
        Regex::new(&args.pattern)?
    };

    // 收集所有文件
    let files: Vec<PathBuf> = WalkDir::new(&args.directory)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().is_file())
        .map(|e| e.path().to_owned())
        .collect();

    // 进度条
    let pb = ProgressBar::new(files.len() as u64);
    pb.set_style(
        ProgressStyle::with_template(
            "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})"
        )?
        .progress_chars("#>-")
    );

    let mut total_matches = 0usize;
    for file in &files {
        pb.inc(1);
        match search_file(file, &regex, &args) {
            Ok(matches) => {
                for m in &matches {
                    print_match(m, &regex, &args);
                }
                total_matches += matches.len();
            }
            Err(e) => {
                eprintln!("{} {}", "警告:".yellow(), e);
            }
        }
    }

    pb.finish_with_message("搜索完成");
    println!(
        "\n{} 在 {} 个文件中找到 {} 处匹配",
        "OK".green().bold(),
        files.len(),
        total_matches
    );

    Ok(())
}

单元测试

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_basic_search() {
        let dir = TempDir::new().unwrap();
        let file_path = dir.path().join("test.txt");
        fs::write(&file_path, "hello world\nfoo bar\nhello rust\n").unwrap();

        let regex = Regex::new("hello").unwrap();
        let args = Args {
            pattern: "hello".into(),
            directory: dir.path().to_str().unwrap().into(),
            context: 0,
            ignore_case: false,
            files_only: false,
        };

        let matches = search_file(&file_path, &regex, &args).unwrap();
        assert_eq!(matches.len(), 2);
        assert_eq!(matches[0].line_number, 1);
        assert_eq!(matches[1].line_number, 3);
    }

    #[test]
    fn test_case_insensitive() {
        let regex = Regex::new("(?i)rust").unwrap();
        assert!(regex.is_match("RUST"));
        assert!(regex.is_match("Rust"));
    }
}

交叉编译与发布

# 添加编译目标
rustup target add x86_64-unknown-linux-musl   # Linux 静态链接
rustup target add x86_64-pc-windows-gnu        # Windows

# 编译
cargo build --release --target x86_64-unknown-linux-musl

# 最终产物 (单文件,无运行时依赖!)
ls -lh target/x86_64-unknown-linux-musl/release/rf
# -rwxr-xr-x  4.2M  rf

使用效果

$ rf "TODO|FIXME" ./src -i
src/main.rs:12   // TODO: add config file support
src/lib.rs:45    // FIXME: handle unicode edge cases
src/utils.rs:89  // TODO: optimize large file reading

OK Found 3 matches across 23 files

Rust 的零成本抽象让 CLI 工具在保持 Python 级开发效率的同时,拥有 C 级的运行性能。下一步可以为 rf 添加 --replace 模式,变成批量文本替换工具。