目标
构建一个命令行文件搜索工具 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: ®ex::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, ®ex, &args) {
Ok(matches) => {
for m in &matches {
print_match(m, ®ex, &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, ®ex, &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模式,变成批量文本替换工具。