参考:https://kaisery.github.io/trpl-zh-cn/ch12-00-an-io-project.html
一位 Rust 社区的成员,Andrew Gallant,已经创建了一个功能完整且非常快速的 grep 版本,叫做 ripgrep (rg)。相比之下,我们的 grep版本将非常简单,本章将教会你一些帮助理解像ripgrep 这样真实项目的背景知识。这帮助你回顾了目前为止的一些主要章节并涉及了如何在 Rust 环境中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和打印错误的 eprintln! 宏。
我们的 grep 项目将会结合之前所学的一些内容:
另外还会简要的讲到闭包、迭代器和 trait 对象,他们分别会在 第十三章 和 第十七章 中详细介绍。
构建代码的思路
- 分离出功能以便每个函数就负责一个任务。当函数承担了更多的功能就意味着承担更多责任,它就更难理解、测试,并且更难以在不破坏其他部分的情况下做出修改。
- 将配置变量组织进一个结构,这样就能使他们的目的更明确了。当作用域中有更多的变量时,将更难以追踪每个变量的目的。
- 把所有的错误处理都放于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码,而且有助于确保我们打印的错误信息对终端用户来说是有意义的。
二进制项目的构建原则
main函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在main函数开始变得庞大时进行二进制程序的 关注分离 (SOC) 的指导性过程。这些过程有如下步骤:
- 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
- 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
- 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。
经过这些过程之后保留在 main 函数中的责任应该被限制为:
- 使用参数值调用命令行解析逻辑
- 设置任何其他的配置
- 调用 lib.rs 中的
run函数 - 如果
run返回错误,则处理这个错误
这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。
从简单的例子入手
在项目主目录下创建 poem.txt 文件(与 Cargo.toml 文件并列),其内容:
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.How dreary to be somebody!How public, like a frogTo tell your name the livelong dayTo an admiring bog!
src/main.rs 内容:
use std::{env, fs};fn main() {let args: Vec<String> = env::args().collect(); // 命令行:cargo run the poem.txtlet [query, filename] = parse_config(&args);let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");println!("\nWith text:\n{}", contents);}fn parse_config(args: &Vec<String>) -> [&str; 2] {let query = &args[1];let filename = &args[2];println!("Searching for {:?}", query);println!("In file {:?}", filename);[query, filename]}
在 main 函数中放入程序的主体,利用函数、方法、甚至泛型等方式抽象和隐藏功能实现的具体过程。
借助类型系统关联数据
比如把配置变量放进元组或者数组,或者依靠相关的名称来关联变量,转换这种方式,让成结构体、枚举体描述数据,会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。
在复杂类型更为合适的场景下使用基本类型的反模式称为 基本类型偏执 (primitive obsession )。
fn main() {let args: Vec<String> = env::args().collect();let (query, filename) = parse_config(&args);// --snip--}fn parse_config(args: &[String]) -> (&str, &str) {let query = &args[1];let filename = &args[2];(query, filename)}
把用户输入获取的配置放入 Config 结构体里(当然,也可以增加其他配置字段),然后充分利用结构体的特点,把 parse_config 这个仅与 Config 有关的函数变成其实例化的方法,这样可以打造具有默认配置的可变配置:
fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args);println!("Searching for {:?}", config.query);println!("In file {:?}", config.filename);let contents =fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("\nWith text:\n{}", contents);}struct Config {query: String,filename: String,}impl Config {fn new(args: &[String]) -> Self { // Self 用来指代当前结构体let query = args[1].clone();let filename = args[2].clone();Self { query, filename }}}
有许多不同的方式可以处理 String 的数据,而最简单但有些不太高效的方式是调用这些值的 clone 方法。这会生成 Config 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。
由于 clone 运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 clone 来解决所有权问题。#todo: 迭代器# 更有效率的处理这种情况,不过现在,复制一些字符串来取得进展是没有问题的,因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。在第一轮编写时拥有一个可以工作但有点低效的程序要比尝试过度优化代码更好一些。随着你对 Rust 更加熟练,将能更轻松的直奔合适的方法,不过现在调用clone 是完全可以接受的。
运行程序:
$ cargo run to poem.txtCompiling minigrep v0.1.0 (/home/ubuntu/scripts/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.40sRunning `target/debug/minigrep to poem.txt`Searching for "to"In file "poem.txt"
改善错误提示信息
尝试不带任何参数运行程序,运行命令 cargo run,或出现 panic 信息 index out of bounds: the len is 1 but the index is 1,因为索引 args vector 时出现问题。这对于使用者来说并不能看懂因为什么出错——程序自身的问题,或者是使用者不恰当使用造成的。
// 覆盖掉上面的 impl 块impl Config {fn new(args: &[String]) -> Self {if args.len() < 3 {panic!("not enough arguments");} // 检查 args 的长度至少是 3// 而函数的剩余部分则可以在假设这个条件成立的基础上运行let query = args[1].clone();let filename = args[2].clone();Self { query, filename }}}
再次运行命令 cargo run,程序依然 panic,但是对使用者来说,现在有了一个合理的错误信息,他们可以知道因为 not enough arguments 导致的。
$ cargo runCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.0 secsRunning `target/debug/minigrep`thread 'main' panicked at 'not enough arguments', src/main.rs:26:13note: Run with `RUST_BACKTRACE=1` for a backtrace.
然而,还是有一堆额外的信息我们不希望提供给用户。panic! 的调用更趋向于程序上的问题而不是使用上的问题。
我们可以使用另一个技术 —— 返回一个可以表明成功或错误的 Result:
use std::{env, fs, process};fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);process::exit(1); // 该函数永远不会返回,并会立即终止当前进程。// 退出代码将传递到底层操作系统,并且可供其他进程使用。// 也不会运行当前堆栈或任何其他线程的堆栈上的析构函数});println!("Searching for {:?}", config.query);println!("In file {:?}", config.filename);let contents =fs::read_to_string(config.filename).expect("Something went wrong reading the file");println!("\nWith text:\n{}", contents);}struct Config {query: String,filename: String,}impl Config {fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let filename = args[2].clone();Ok(Config { query, filename })}}
unwrap_or_else,它定义于标准库的 Result<T, E> 上(Option 上也被定义)。使用 unwrap_or_else 可以进行一些自定义的非panic!的错误处理。当 Result 是 Ok 时,这个方法的行为类似于 unwrap:它返回 Ok 内部封装的值。然而,当其值是 Err 时,该方法会需要 函数或者 闭包 (closure ,匿名函数)作为参数。
现在运行命令 cargo run,报错信息对于用户来说就友好多了。
$ cargo runCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.48 secsRunning `target/debug/minigrep`Problem parsing arguments: not enough arguments
还有一处使用了 expect 默认处理错误:read_to_string 方法返回 Result<String, Error>。现在修改这一处错误信息,让它提示更人性化,而不是让使用者看到冗长的 panic 信息。最终单个 main.rs 文件内的代码如下:
use std::{env, error::Error, fs, process};fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);process::exit(1);});println!("Searching for {:?}", config.query);println!("In file {:?}", config.filename);// `if let 模式 = 表达式` 语法可以用来处理 只匹配一个模式 的值而忽略其他模式的情况// 如果 `run` 返回 Ok 类型,正常处理完读取、打印文件,进行到 if let 时模式不匹配,主程序往下运行// 如果读取失败,返回 Err 类型,进入 if let 模式匹配,打印错误,主程序退出if let Err(e) = run(config) { // 这里的 e 类比 unwrap_or_else 里面的闭包参数 errprintln!("Application error: {}", e);process::exit(1);};}struct Config {query: String,filename: String,}impl Config {fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let filename = args[2].clone();Ok(Config { query, filename })}}fn run(config: Config) -> Result<(), Box<dyn Error>> {// Box<dyn Error> 表示函数会返回实现了 Error trait 的类型,不过无需指定具体将会返回的值的类型。// 这使得 在不同的错误场景允许有不同类型的错误返回值,程序更加灵活。// dyn = dynamiclet contents = fs::read_to_string(config.filename)?;println!("\nWith text:\n{}", contents);Ok(())}
尝试读取不存在的文件,可以看到自定义的输出提示:
$ cargo run the file_not_exist.txtCompiling minigrep v0.1.0 (/home/ubuntu/scripts/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.43sRunning `target/debug/minigrep the file_not_exist.txt`Searching for "the"In file "file_not_exist.txt"Application error: No such file or directory (os error 2)
例子中 if let 和 unwrap_or_else 的函数体都一样:打印出错误并退出。它们代表两种方式,用于替换处理错误时简单粗暴的 unwrap 或者 except 方法。
将 main.rs 拆分进 lib.rs
我们知道,一个 “package” (也就是一个项目)可以允许一个 lib crate 和多个 binary crate。lib 和 binary 的 crate 名称就是项目主目录的名称(即与 package 同名)。而且”在 main.rs 中使用 use 把同名的 lib crate 的功能引入作用域”。
// src/main.rs// minigrep 作为同名 lib crate,无需手动引入。但里面的功能为了减少名称长度,可以单独引入作用域use minigrep::Config;use std::{env, process}; // 不在当前文件中使用的功能无需引入,比如 error::Error, fsfn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);process::exit(1);});println!("Searching for {:?}", config.query);println!("In file {:?}", config.filename);if let Err(e) = minigrep::run(config) { // minigrep 默认被引入作用域println!("Application error: {}", e);process::exit(1);};}
// src/lib.rsuse std::{error::Error, fs}; // 不在当前文件中使用的功能无需引入,比如 env, processpub struct Config {pub query: String,pub filename: String,}impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let filename = args[2].clone();Ok(Config { query, filename })}}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.filename)?;println!("\nWith text:\n{}", contents);Ok(())}
至此,我们把 “冗长的 main.rs” 拆分成两个文件,而且利用 lib.rs 进行功能拓展,比如测试、编写新的核心代码。
测试驱动开发 (TTD)
测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 minigrep 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤:
- 编写一个失败的测试,并运行它以确保它失败的原因是你所期望的。
- 编写或修改足够的代码来使新的测试通过。
- 重构刚刚增加或修改的代码,即在实际的运行代码中调用编写好的函数,并确保测试仍然能通过。
- 从步骤 1 开始重复!
这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。
增加搜索功能
查找包含传入的查询字符串所在的行,打印符合的行内容
// src/lib.rsuse std::{error::Error, fs};pub struct Config {pub query: String,pub filename: String,}impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let filename = args[2].clone();Ok(Config { query, filename })}}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(&config.filename)?;// println!("\nWith text:\n{}", contents);// 步骤 3for line in search(&config.query, &contents) {println!("{}", line);}Ok(())}// 步骤 2pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut find: Vec<&str> = Vec::new();for line in contents.lines() {if line.contains(query) {find.push(line);}}find}// 步骤 1#[cfg(test)]mod tests {use super::*;#[test]fn one_result() {let query = "duct";let contents = "\Rust:safe, fast, productive.Pick three."; // `\` 符号表示去除换行,`\` 的下一行的内容接着 `\` 前的内容assert_eq!(vec!["safe, fast, productive."], search(query, contents));}}
支持大小写不敏感:参数方式
依然打印包含传入值所在的行。
使用环境变量CASE_INSENSITIVE来指定大小写不敏感,未指定时默认大小写敏感。
// src/lib.rsuse std::{env, error::Error, fs};#[derive(Debug)]pub struct Config {pub query: String,pub filename: String,pub case_sensitive: bool, // 增加一个配置字段}impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}// 获取环境变量,注意这里的逻辑是 CASE_INSENSITIVE 存在于环境变量中,则认为不敏感// 无论 CASE_INSENSITIVE=0 还是等于任意数字、甚至任意字符,都认为是大小写不敏感// 这里这么做是为了简便let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); // env::var 返回 Result 类型let query = args[1].clone();let filename = args[2].clone();Ok(Config {query,filename,case_sensitive,})}}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(&config.filename)?;let find = if config.case_sensitive {search(&config.query, &contents)} else {search_case_insensitive(&config.query, &contents)}; // 利用新增的 大小写敏感 字段值来选择哪种函数for line in find {println!("{}", line);}Ok(())}pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut find: Vec<&str> = Vec::new();// line 必须严格包括 query 子串,所以这是大小写敏感的for line in contents.lines() {if line.contains(query) {find.push(line);}}find}// 新增一个支持大小写不敏感的函数,统一把不大小写敏感的内容转换成小写pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut find: Vec<&str> = Vec::new();let query_lower = query.to_lowercase();for line in contents.lines() {if line.to_lowercase().contains(&query_lower) {find.push(line);}}find}#[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));}}
运行程序:
大小写不敏感(存在 CASE_INSENSITIVE 环境变量)
$ CASE_INSENSITIVE=1 cargo run to poem.txtCompiling minigrep v0.1.0 (/home/ubuntu/scripts/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.40sRunning `target/debug/minigrep to poem.txt`Searching for "to"In file "poem.txt"Are you nobody, too?How dreary to be somebody!To tell your name the livelong dayTo an admiring bog!
大小写敏感 (无 CASE_INSENSITIVE 环境变量)
$ cargo run to poem.txtFinished dev [unoptimized + debuginfo] target(s) in 0.01sRunning `target/debug/minigrep to poem.txt`Searching for "to"In file "poem.txt"Are you nobody, too?How dreary to be somebody!
支持大小写不敏感:环境变量
也可以使用命令行参数来指定不敏感,比如 -i 表示 大小写不敏感 (case insensitive),-I 表示 大小写敏感 (因为强调了大写),那么修改一下 Config::new 方法即可:
// src/lib.rs// 下面覆盖相应的代码impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 4 {return Err("not enough arguments");}let case_sensitive = match args[1].as_str() {"-i" => false,"-I" => true,_ => true,};let query = args[2].clone(); ‣query: Stringlet filename = args[3].clone(); ‣filename: StringOk(Config {query,filename,case_sensitive,})}}
由于 -i 之类带 - 的参数直接跟在 cargo run 命令会产生冲突,所以先编译,然后手动指定运行 minigrep 程序:
$ cargo build$ ./target/debug/minigrep -i to poem.txt # 大小写不敏感Searching for "to"In file "poem.txt"Are you nobody, too?How dreary to be somebody!To tell your name the livelong dayTo an admiring bog!$ ./target/debug/minigrep -I to poem.txt # 大小写敏感Searching for "to"In file "poem.txt"Are you nobody, too?How dreary to be somebody!$ ./target/debug/minigrep to poem.txt # 必须传入三个参数Problem parsing arguments: not enough arguments
依然可以改进,第 1 个参数是 - 开头的字符串时被识别是控制搜索方式的参数;第 1 个参数不已 - 开头,则任务以大小写敏感方式接受查询值和查询文件。如此便可以输入两个或者三个参数都能运行:
// src/lib.rs// 下面覆盖相应的代码impl Config {pub fn new(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let mut args = args.to_vec();let case_sensitive = if args[1].starts_with("-") {match args.remove(1).as_str() {"-i" => false,"-I" => true,_ => true, // 这里需要做成错误处理,或者识别其他传入的参数}} else {true};let query = args[1].clone();let filename = args[2].clone();Ok(Config {query,filename,case_sensitive,})}}
运行程序:
$ cargo build$ ./target/debug/minigrep to poem.txt # 可以传入两个参数时默认大小写敏感Searching for "to"In file "poem.txt"Are you nobody, too?How dreary to be somebody!
处理环境变量与参数之间的冲突
一些程序允许对相同配置同时使用参数 和 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。
笔者修改了 Config::new 方法,支持:
- 环境变量
CASE_INSENSITIVE=1时被认为是大小写不敏感,其他值都表示大小写敏感 环境变量和参数都存在时,以参数为准。 ``` // src/lib.rs // 下面覆盖相应的代码 impl Config { pub fn new(args: &[String]) -> Result<Config, &’static str> {
if args.len() < 3 {return Err("not enough arguments");}let mut args = args.to_vec();let case_sensitive = if args[1].starts_with("-") {match args.remove(1).as_str() {"-i" => false,"-I" => true,_ => true, // 这里可以做成错误处理,或者识别其他传入的参数}} else {match env::var("CASE_INSENSITIVE") {// 当 CASE_INSENSITIVE=1 时才被认为是大小不敏感Ok(s) if s == 1.to_string() => false,_ => true,}};let query = args[1].clone();let filename = args[2].clone();Ok(Config {query,filename,case_sensitive,})
} }
[重构了测试代码](https://gitee.com/ZIP97/minigrep/tree/v0.1/tests):
$ cargo test running 6 tests test tests_case_sens::case_insensitive_conflict … ok test tests_case_sens::case_insensitive_env_var … ok test tests_case_sens::case_insensitive_no_env_var … ok test tests_case_sens::case_sensitive_conflict … ok test tests_search::case_insensitive … ok test tests_search::case_sensitive … ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
最终项目内容见:[https://gitee.com/ZIP97/minigrep/tree/v0.1](https://gitee.com/ZIP97/minigrep/tree/v0.1),二进制软件运行情况:
$ cargo build
$ ./target/debug/minigrep # 提示友好,而不是 panic 信息 Problem parsing arguments: not enough arguments
$ ./target/debug/minigrep to poem.txt # 不强制输入参数 Are you nobody, too? How dreary to be somebody!
$ ./target/debug/minigrep -i to poem.txt # 支持参数 Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog!
$ CASE_INSENSITIVE=1 ./target/debug/minigrep to poem.txt # 支持环境变量 Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog!
$ CASE_INSENSITIVE=1 ./target/debug/minigrep -i to poem.txt # 支持环境变量与参数共存 Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog!
$ CASE_INSENSITIVE=0 ./target/debug/minigrep -I to poem.txt # 支持环境变量与参数共存 Are you nobody, too? How dreary to be somebody!
$ CASE_INSENSITIVE=0 ./target/debug/minigrep -i to poem.txt # 环境变量与参数冲突时,参数优先 Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog!
$ CASE_INSENSITIVE=1 ./target/debug/minigrep -I to poem.txt # 环境变量与参数冲突时,参数优先 Are you nobody, too? How dreary to be somebody!
## 程序结果输出到文件 与 `stderr`目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:|| stdout| stderr|| --- | --- | --- ||Rust 宏| `println!`| `eprintln!`||不输出到文件时| 正常打印| 正常打印||使用 `>` 输出到文件| 支持| 不支持||输出到文件时| 覆盖/创建输出的文件,把打印的内容写入文件,打印内容不再显示| 覆盖/创建输出的文件,不会把打印的内容写入文件,打印内容正常显示|- **标准输出** (_standard output_ ,`stdout`)对应一般信息,除了正常打印之外,如果在终端命令的最后使用 `> output.txt` 把原本正常打印的内容输出到文件,此时终端不显示打印内容。Rust 中使用 `println!` 宏。- **标准错误** (_standard error_ ,`stderr`)则用于打印错误信息,只能打印,无法输出内容到文件。即使在运行命令后面添加了 `> output.txt` 命令,也不会把内容输出到文件,仅仅是创建/覆盖 output.txt 文件而已。Rust 中使用 `eprintln!` 宏。让我们最后对 `src/main.rs` 做调整:如果程序正常运行,运行结果输出成文件;如果运行错误,打印错误信息,输出文件为空
// src/main.rs use minigrep::Config; use std::{env, process};
fn main() {
let args: Vec
let config = Config::new(&args).unwrap_or_else(|err| {eprintln!("Problem parsing arguments: {}", err);process::exit(1);});// 删除了打印查询文字和查询文件的信息if let Err(e) = minigrep::run(config) {eprintln!("Application error: {}", e); // 这是 stderr ,不会输出到文件process::exit(1);};
}
程序正常运行并输出到文件:
$ cargo build
$ ./target/debug/minigrep to poem.txt > output.txt
$ cat output.txt Are you nobody, too? How dreary to be somebody!
程序错误时打印,输出文件为空:
$ ./target/debug/minigrep > output.txt Problem parsing arguments: not enough arguments $ cat output.txt
```
