Skip to content

Day 30: 实战项目:命令行工具

今天是30天Rust学习的收官之日,我们将综合运用所学知识,构建一个完整的命令行工具:rusty-find——一个快速的文件搜索工具。

这个工具将展示:

  • 命令行参数解析(clap)
  • 文件和目录遍历
  • 错误处理
  • 并发搜索(使用Arc和thread)
  • 发布到crates.io

首先创建一个新项目:

Terminal window
cargo new rusty-find
cd rusty-find

添加依赖到Cargo.toml:

[dependencies]
clap = { version = "4", features = ["derive"] }
anyhow = "1"
walkdir = "2"
rayon = "1"
  • clap:命令行参数解析
  • anyhow:灵活的错误处理
  • walkdir:目录遍历
  • rayon:数据并行库,简化并发

使用clap的derive模式定义参数:

src/main.rs
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// 搜索的起始目录
#[arg(default_value = ".")]
path: String,
/// 要搜索的文件名
pattern: String,
/// 是否大小写敏感
#[arg(short, long, default_value = "false")]
case_sensitive: bool,
/// 最大搜索深度
#[arg(short, long, default_value_t = 10)]
max_depth: usize,
}
fn main() {
let args = Args::parse();
println!("搜索参数: {:?}", args);
}

运行cargo run -- "src" "clap"测试基本参数解析。

use anyhow::{Context, Result};
use std::path::Path;
use walkdir::WalkDir;
fn search_files(
root: &Path,
pattern: &str,
case_sensitive: bool,
max_depth: usize,
) -> Result<Vec<String>> {
let mut matches = Vec::new();
for entry in WalkDir::new(root)
.max_depth(max_depth)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
// 只处理文件,跳过目录
if !path.is_file() {
continue;
}
// 获取文件名
let file_name = match path.file_name() {
Some(name) => name.to_string_lossy(),
None => continue,
};
// 检查是否匹配
let file_name_str = file_name.as_ref();
let matches_pattern = if case_sensitive {
file_name_str.contains(pattern)
} else {
file_name_str.to_lowercase().contains(&pattern.to_lowercase())
};
if matches_pattern {
matches.push(path.display().to_string());
}
}
Ok(matches)
}

使用anyhow进行链式错误处理:

use anyhow::{Context, Result};
fn read_config() -> Result<()> {
let content = std::fs::read_to_string("config.toml")
.with_context(|| format!("Failed to read config.toml"))?;
println!("配置内容: {}", content);
Ok(())
}

使用rayon实现并行搜索:

use rayon::prelude::*;
use std::path::Path;
fn parallel_search(
root: &Path,
pattern: &str,
case_sensitive: bool,
max_depth: usize,
) -> Result<Vec<String>> {
// 收集所有文件路径
let files: Vec<_> = WalkDir::new(root)
.max_depth(max_depth)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.map(|e| e.path().to_path_buf())
.collect();
// 并行搜索
let matches = files
.par_iter()
.filter_map(|path| {
let file_name = path.file_name()?.to_string_lossy();
let file_name_str = file_name.as_ref();
let matches_pattern = if case_sensitive {
file_name_str.contains(pattern)
} else {
file_name_str.to_lowercase().contains(&pattern.to_lowercase())
};
if matches_pattern {
Some(path.display().to_string())
} else {
None
}
})
.collect();
Ok(matches)
}

rayon的.par_iter()自动将工作分配到多个线程,非常适合IO密集型任务。

use anyhow::Result;
use clap::Parser;
use std::path::Path;
use std::time::Instant;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(default_value = ".")]
path: String,
pattern: String,
#[arg(short, long, default_value = "false")]
case_sensitive: bool,
#[arg(short, long, default_value_t = 10)]
max_depth: usize,
#[arg(long, default_value = "false")]
parallel: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let path = Path::new(&args.path);
if !path.exists() {
anyhow::bail!("路径不存在: {}", args.path);
}
let start = Instant::now();
let matches = if args.parallel {
println!("使用并行搜索...");
parallel_search(path, &args.pattern, args.case_sensitive, args.max_depth)?
} else {
println!("使用单线程搜索...");
search_files(path, &args.pattern, args.case_sensitive, args.max_depth)?
};
let elapsed = start.elapsed();
if matches.is_empty() {
println!("没有找到匹配的文件");
} else {
println!("\n找到 {} 个匹配:", matches.len());
for m in &matches {
println!(" {}", m);
}
}
println!("\n耗时: {:?}", elapsed);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_case_sensitive_match() {
let result = "Hello.txt".contains("Hello");
assert!(result);
}
#[test]
fn test_case_insensitive_match() {
let file = "Hello.txt".to_lowercase();
let pattern = "hello".to_lowercase();
assert!(file.contains(&pattern));
}
}

运行cargo test确保测试通过。

发布前需要:

  1. 在crates.io注册账号
  2. 获取API token
  3. 配置cargo登录
Terminal window
# 登录
cargo login <your-api-token>
# 检查代码
cargo publish --dry-run
# 发布
cargo publish

今天我们构建了一个功能完整的命令行工具,涵盖了:

  • clap:优雅的命令行参数解析
  • walkdir:可靠的目录遍历
  • rayon:简洁的并发搜索
  • anyhow:链式错误处理
  • 测试与发布:完整的开发生态

这只是Rust能力的冰山一角。接下来你可以:

  • 添加正则表达式支持
  • 实现文件内容搜索
  • 添加进度条
  • 支持多平台

30天的学习让你掌握了Rust的核心技能。继续实践,探索更深的领域!