Rust 入门系列(第 4 篇,共 5 篇) 前置阅读:第 3 篇 - 所有权、借用与生命周期 阅读时间:20 分钟
引言
前三篇学习了 Rust 的基础知识,这一篇我们来做一个实战项目:构建一个命令行文件搜索工具。
这个项目会用到:
- ✅ Clap:解析命令行参数
- ✅ 文件 I/O:读取文件内容
- ✅ 错误处理:Result 和 ? 操作符
- ✅ 迭代器和闭包
一、项目介绍
1.1 功能需求
构建一个类似 grep 的工具 minigrep:
bash
1
cargo run -- query filename.txt功能:
- 在文件中搜索指定字符串
- 显示包含该字符串的所有行
- 支持大小写不敏感搜索
1.2 创建项目
bash
12
cargo new minigrep
cd minigrep二、基础版本
2.1 读取命令行参数
rust
1234567
// src/main.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}运行:
bash
12
$ cargo run -- query filename.txt
["target/debug/minigrep", "query", "filename.txt"]2.2 提取参数
rust
123456789
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("搜索: {}", query);
println!("文件: {}", file_path);
}2.3 读取文件
rust
12345678910111213141516
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("搜索: {}", query);
let contents = fs::read_to_string(file_path)
.expect("无法读取文件");
println!("文件内容:\n{}", contents);
}测试:
创建 poem.txt:
plaintext
1234
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.运行:
bash
1
$ cargo run -- nobody poem.txt三、重构:提高可维护性
3.1 提取配置
rust
12345678910111213141516171819202122232425
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("搜索: {}", config.query);
let contents = fs::read_to_string(config.file_path)
.expect("无法读取文件");
println!("文件内容:\n{}", contents);
}3.2 错误处理
rust
1234567891011121314151617181920212223
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("参数不足");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("参数解析错误: {}", err);
std::process::exit(1);
});
// ...
}3.3 提取核心逻辑
rust
1234567891011121314151617181920212223242526272829303132333435
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("参数解析错误: {}", err);
std::process::exit(1);
});
if let Err(e) = run(config) {
println!("应用错误: {}", e);
std::process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn std::error::Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}运行:
bash
123
$ cargo run -- nobody poem.txt
I'm nobody! Who are you?
Are you nobody, too?四、使用 Clap 改进
4.1 添加依赖
toml
123
# Cargo.toml
[dependencies]
clap = { version = "4.4", features = ["derive"] }4.2 使用 Clap
rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
use clap::Parser;
use std::fs;
#[derive(Parser, Debug)]
#[command(name = "minigrep")]
#[command(about = "A simple grep clone in Rust", long_about = None)]
struct Args {
/// 搜索的字符串
query: String,
/// 文件路径
file_path: String,
/// 大小写不敏感搜索
#[arg(short, long)]
ignore_case: bool,
}
fn main() {
let args = Args::parse();
if let Err(e) = run(args) {
eprintln!("错误: {}", e);
std::process::exit(1);
}
}
fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
let contents = fs::read_to_string(&args.file_path)?;
let results = if args.ignore_case {
search_case_insensitive(&args.query, &contents)
} else {
search(&args.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
contents
.lines()
.filter(|line| line.to_lowercase().contains(&query))
.collect()
}使用示例:
bash
12345678
# 普通搜索
$ cargo run -- nobody poem.txt
# 大小写不敏感
$ cargo run -- NOBODY poem.txt -i
# 查看帮助
$ cargo run -- --help五、测试
5.1 添加测试
rust
12345678910111213141516171819202122232425262728293031
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}运行测试:
bash
1
cargo test六、完整代码
6.1 main.rs
rust
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
use clap::Parser;
use std::fs;
use std::process;
#[derive(Parser, Debug)]
#[command(name = "minigrep")]
#[command(about = "A simple grep clone in Rust")]
struct Args {
/// 搜索的字符串
query: String,
/// 文件路径
file_path: String,
/// 大小写不敏感搜索
#[arg(short, long)]
ignore_case: bool,
}
fn main() {
let args = Args::parse();
if let Err(e) = run(args) {
eprintln!("应用错误: {}", e);
process::exit(1);
}
}
fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
let contents = fs::read_to_string(&args.file_path)?;
let results = if args.ignore_case {
search_case_insensitive(&args.query, &contents)
} else {
search(&args.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
contents
.lines()
.filter(|line| line.to_lowercase().contains(&query))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "Rust:\nsafe, fast, productive.\nPick three.\nDuct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "Rust:\nsafe, fast, productive.\nPick three.\nTrust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}七、扩展功能
7.1 添加行号
rust
12345678
fn search_with_line_numbers<'a>(query: &str, contents: &'a str) -> Vec<(usize, &'a str)> {
contents
.lines()
.enumerate()
.filter(|(_, line)| line.contains(query))
.map(|(i, line)| (i + 1, line))
.collect()
}7.2 正则表达式支持
toml
123
[dependencies]
clap = { version = "4.4", features = ["derive"] }
regex = "1.10"rust
123456789
use regex::Regex;
fn search_regex<'a>(pattern: &str, contents: &'a str) -> Vec<&'a str> {
let re = Regex::new(pattern).expect("无效的正则表达式");
contents
.lines()
.filter(|line| re.is_match(line))
.collect()
}7.3 多文件搜索
rust
123456789101112131415161718192021222324
use std::fs;
use std::path::PathBuf;
#[derive(Parser)]
struct Args {
query: String,
/// 文件路径(可以多个)
#[arg(required = true)]
files: Vec<PathBuf>,
}
fn main() {
let args = Args::parse();
for file_path in &args.files {
println!("\n文件: {:?}", file_path);
let contents = fs::read_to_string(file_path).expect("读取失败");
let results = search(&args.query, &contents);
for line in results {
println!("{}", line);
}
}
}八、学到了什么?
8.1 核心技能
- 命令行参数解析:使用 Clap
- 文件 I/O:
fs::read_to_string - 错误处理:
Result和?操作符 - 迭代器:
lines(),filter(),collect() - 生命周期:函数返回引用
- 测试:单元测试
8.2 Rust 最佳实践
- ✅ 早期失败:参数错误立即退出
- ✅ 错误传播:使用
?简化代码 - ✅ 迭代器优先:避免手动循环
- ✅ 测试覆盖:核心逻辑都有测试
九、下一步
9.1 练习建议
- 添加更多选项(上下文行数、高亮显示等)
- 支持递归目录搜索
- 添加性能基准测试
9.2 推荐资源
系列总结
已完成:
- Rust 是什么?能做什么?
- 基础语法与核心概念
- 所有权、借用与生命周期
- 实战项目 - CLI 工具
下一篇(最后一篇):《Rust 入门(五):Rust + WebAssembly》
参考资料
- Getting Started with Clap - DEV Community
- Writing a CLI Tool with Clap - Shuttle
- Build a Rust CLI Tutorial - Codez Up
- The Rust Programming Language Book - Chapter 12