8. 文件与命令行篇¶
对应章节:第12章(I/O 项目)。
本篇掌握命令行解析、文件读取、环境变量与规范的错误输出。
项目 15:迷你 grep¶
目标:命令行 + 文件读取 + 环境变量,规范的错误处理。
创建项目¶
功能要求¶
- 搜索文件中的匹配行
- 支持大小写不敏感模式(环境变量控制)
- 错误用
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
注意事项¶
- 业务逻辑放 lib,main 只做调度:
search等在 lib.rs 可测试;main.rs 处理参数与错误——关注点分离。 Config::build返回Result:参数校验不 panic,返回&'static str错误信息,unwrap_or_else统一处理。eprintln!vsprintln!:错误信息输出到 stderr,正常结果输出到 stdout——这样cargo run -- x f | grep xxx时错误不会混入管道。CASE_INSENSITIVE=1环境变量:env::var存在即为开启(值无所谓),无需解析布尔值。- 生命周期
'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 里,只能整体测 |