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

功能点:

  1. 读取命令行参数。
  2. 读取文件内容。
  3. 筛选包含关键词的行。
  4. 错误处理(用户没传参数?文件不存在?)。
  5. 关注点分离main.rs 负责处理参数和系统调用,lib.rs 负责核心逻辑。
  6. TDD:测试驱动开发。
  7. 环境变量:支持 IGNORE_CASE=1 进行大小写不敏感搜索。

2. 核心代码演进

我们将代码分为 main.rslib.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::buildsearch 中的流式处理。
  • 错误处理Resulteprintln!
  • Refactoring:从过程式代码重构为更 Rust 的风格。

下一篇,我们将进入 并发编程 (Concurrency) 的世界。Rust 标榜的 “Fearless Concurrency” 到底神在哪里?


练习题

  1. 修改 run 函数,使其返回一个自定义的错误类型,而不是 Box<dyn Error>
  2. 添加一个新的功能:打印行号。

思考题

eprintln!println! 有什么区别?为什么在命令行工具中区分 stdout 和 stderr 很重要?


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读