跳转至

8. 文件与命令行篇

对应章节:第12章(I/O 项目)。
本篇掌握命令行解析、文件读取、环境变量与规范的错误输出。


项目 15:迷你 grep

目标:命令行 + 文件读取 + 环境变量,规范的错误处理。

创建项目

cargo new minigrep
cd minigrep

功能要求

cargo run -- <查询词> <文件名>
CASE_INSENSITIVE=1 cargo run -- <查询词> <文件名>
  • 搜索文件中的匹配行
  • 支持大小写不敏感模式(环境变量控制)
  • 错误用 eprintln! 输出到 stderr

知识点

知识点 说明
库 + bin 分离 lib.rs 放逻辑,main.rs 只调度
env::var 读取环境变量
CASE_INSENSITIVE 开关模式
eprintln! 错误输出到 stderr
Result<(), Box<dyn Error>> main 返回错误
生命周期 'a 返回值绑定输入

代码

src/lib.rs:

// 搜索逻辑放库:可测试、可复用
// 生命周期 'a:返回的 &str 引用 contents,用 'a 连接两者保证安全
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))   // 保留包含 query 的行
        .collect()
}

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    // 先把 query 转小写(一次),每行比较时也转小写
    let query = query.to_lowercase();
    contents.lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn search_works() {
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(search("Rust", contents), vec!["Rust:"]);
        assert_eq!(search("safe", contents), vec!["safe, fast, productive."]);
    }

    #[test]
    fn case_insensitive_works() {
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        // 大写 RUST 也能匹配到 "Rust:"
        assert_eq!(
            search_case_insensitive("rust", contents),
            vec!["Rust:"]
        );
    }
}

src/main.rs:

use std::env;
use std::fs;
use std::process;
use minigrep::search;
use minigrep::search_case_insensitive;

// 业务逻辑独立为 run,main 只做"参数解析 + 错误出口"
fn main() {
    let args: Vec<String> = env::args().collect();

    // Config::build 返回 Result;错误时打印到 stderr 并退出码 1
    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("参数错误:{}", err);   // eprintln! 输出到 stderr
        process::exit(1);                 // 非零退出码表示失败
    });

    // run 返回 Result;失败同样打印到 stderr
    if let Err(e) = run(&config) {
        eprintln!("运行错误:{}", e);
        process::exit(1);
    }
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    // 参数校验放在 build 中,返回 Result 而非直接 panic
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("用法:cargo run -- <查询词> <文件名>");
        }
        Ok(Config {
            query: args[1].clone(),    // 从切片克隆 String(args 是别人的)
            filename: args[2].clone(),
        })
    }
}

fn run(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
    // ? 传播 IO 错误
    let contents = fs::read_to_string(&config.filename)?;

    // 环境变量存在即为大小写不敏感(值无所谓)
    let results = if env::var("CASE_INSENSITIVE").is_ok() {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);   // 正常结果输出到 stdout
    }
    Ok(())
}

运行

echo "Rust is great
rust programming
Hello World" > poem.txt

cargo run -- rust poem.txt            # 只匹配 "rust"
CASE_INSENSITIVE=1 cargo run -- rust poem.txt   # 匹配 Rust 和 rust

注意事项

  1. 业务逻辑放 lib,main 只做调度:search 等在 lib.rs 可测试;main.rs 处理参数与错误——关注点分离。
  2. Config::build 返回 Result:参数校验不 panic,返回 &'static str 错误信息,unwrap_or_else 统一处理。
  3. eprintln! vs println!:错误信息输出到 stderr,正常结果输出到 stdout——这样 cargo run -- x f | grep xxx 时错误不会混入管道。
  4. CASE_INSENSITIVE=1 环境变量:env::var 存在即为开启(值无所谓),无需解析布尔值。
  5. 生命周期 'a:search 返回的 &str 引用 contents,必须声明 'a 连接二者。

进阶:精简版 minigrep

功能完全一致(lib.rs 的两个搜索函数与原版几乎一字不差),区别在 main.rs 的组织方式——没有 Config 结构和 run 函数,直接让 main 返回 Result:

src/main.rs:

use std::env;
use std::fs;
use std::process;

use minigrep::search;
use minigrep::search_case_insensitive;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = env::args().collect();

    if args.len() < 3 {
        eprintln!("Usage: minigrep <query> <filename>");
        process::exit(1);
    }

    let query = &args[1];
    let path = &args[2];

    let contents = fs::read_to_string(path)?;

    let case_insensitive = env::var("CASE_INSENSITIVE").is_ok();

    let matches = if case_insensitive {
        search_case_insensitive(query, &contents)
    } else {
        search(&query.to_lowercase(), &contents)
    };

    // 带序号输出匹配行
    for (i, line) in matches.iter().enumerate() {
        println!("{}: {}", i + 1, line);
    }
    Ok(())
}

(lib.rs 与原版相同的部分此处不重复;单元测试同样通过。)

两种写法对比

原版(Config + run) 进阶版(Result main)
参数校验 Config::build 返回 Result,错误信息可定制 手动 if + eprintln! + exit(1)
IO 错误 run 内 ? 后由 main 统一打印 main 直接 ?,出错时打印错误的 Debug 格式
代码量 多一层抽象,适合继续长大的项目 直白短小,小工具够用
可测试性 run(config) 可被测试直接调用 逻辑都在 main 里,只能整体测

评论