Rust 学习笔记 20:项目实战二:minigrep
“UNIX is basically a simple operating system, but you have to be a genius to understand the simplicity.” – Dennis Ritchie
到目前为止,我们已经学习了 Rust 的大部分核心特性。现在,让我们把它们串起来,复刻一个经典的命令行工具:grep。
我们的目标是创建一个 minigrep,它接受一个查询字符串和一个文件名,然后打印出文件中包含查询字符串的行。
1. 需求分析
用法:
1$ cargo run -- searchstring example-filename.txt
功能点:
- 读取命令行参数。
- 读取文件内容。
- 筛选包含关键词的行。
- 错误处理(用户没传参数?文件不存在?)。
- 关注点分离:
main.rs负责处理参数和系统调用,lib.rs负责核心逻辑。 - TDD:测试驱动开发。
- 环境变量:支持
IGNORE_CASE=1进行大小写不敏感搜索。
2. 核心代码演进
我们将代码分为 main.rs 和 lib.rs。
2.1 参数解析与配置
在 src/lib.rs 中定义 Config 结构体:
1use std::env;
2
3pub struct Config {
4 pub query: String,
5 pub file_path: String,
6 pub ignore_case: bool,
7}
8
9impl Config {
10 pub fn build(mut args: impl Iterator<Item = String>) -> Result<Config, &'static str> {
11 args.next(); // 也就是程序名,通常忽略
12
13 let query = match args.next() {
14 Some(arg) => arg,
15 None => return Err("Didn't get a query string"),
16 };
17
18 let file_path = match args.next() {
19 Some(arg) => arg,
20 None => return Err("Didn't get a file path"),
21 };
22
23 let ignore_case = env::var("IGNORE_CASE").is_ok();
24
25 Ok(Config {
26 query,
27 file_path,
28 ignore_case,
29 })
30 }
31}
注意这里使用了 impl Iterator,这样我们可以直接消费 env::args(),更加高效。
2.2 搜索逻辑 (使用生命周期)
1pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
2 contents
3 .lines()
4 .filter(|line| line.contains(query))
5 .collect()
6}
这里必须标注生命周期 'a,告诉编译器:返回的字符串切片引用的是 contents 里的数据,而不是 query。
2.3 错误处理
在 main.rs 中,我们不再直接 panic,而是优雅地打印错误并退出:
use std::env;
use std::process;
use ch20_minigrep::Config;
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = ch20_minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
使用 eprintln! 将错误信息输出到标准错误流 (stderr),这样即使重定向了 stdout,错误信息也能显示在屏幕上。
3. 测试驱动开发 (TDD)
我们先写一个失败的测试:
1#[test]
2fn case_sensitive() {
3 let query = "duct";
4 let contents = "\
5Rust:
6safe, fast, productive.
7Pick three.";
8
9 assert_eq!(vec!["safe, fast, productive."], search(query, contents));
10}
然后实现 search 函数让测试通过。Rust 的测试反馈循环非常快。
4. 小结
第20篇笔记。 这个项目虽然小,但"五脏俱全"。它涵盖了:
- 模块化:lib vs main 分离。
- 所有权与借用:Config 的字段所有权处理。
- 生命周期:
search函数返回值的生命周期。 - 迭代器:
Config::build和search中的流式处理。 - 错误处理:
Result和eprintln!。 - Refactoring:从过程式代码重构为更 Rust 的风格。
下一篇,我们将进入 并发编程 (Concurrency) 的世界。Rust 标榜的 “Fearless Concurrency” 到底神在哪里?
练习题:
- 修改
run函数,使其返回一个自定义的错误类型,而不是Box<dyn Error>。 - 添加一个新的功能:打印行号。
思考题:
eprintln! 和 println! 有什么区别?为什么在命令行工具中区分 stdout 和 stderr 很重要?
本文代码示例:
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
