From 324244de9b6f890819c29c9e8a8dfcc44706e3c3 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 9 Feb 2022 16:54:09 +0800 Subject: [PATCH] update to ch12-06 --- .../listing-12-17/src/lib.rs | 2 +- .../listing-12-18/src/lib.rs | 2 +- ...h12-01-accepting-command-line-arguments.md | 50 ++-- src/ch12-02-reading-a-file.md | 52 +--- ...improving-error-handling-and-modularity.md | 228 ++---------------- ...2-04-testing-the-librarys-functionality.md | 154 +++--------- ...2-05-working-with-environment-variables.md | 154 ++---------- ...-06-writing-to-stderr-instead-of-stdout.md | 27 +-- 8 files changed, 104 insertions(+), 565 deletions(-) diff --git a/listings/ch12-an-io-project/listing-12-17/src/lib.rs b/listings/ch12-an-io-project/listing-12-17/src/lib.rs index 0b094ce..5c467f6 100755 --- a/listings/ch12-an-io-project/listing-12-17/src/lib.rs +++ b/listings/ch12-an-io-project/listing-12-17/src/lib.rs @@ -28,7 +28,7 @@ pub fn run(config: Config) -> Result<(), Box> { // ANCHOR: here pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { - // do something with line + // 对文本行进行操作 } } // ANCHOR_END: here diff --git a/listings/ch12-an-io-project/listing-12-18/src/lib.rs b/listings/ch12-an-io-project/listing-12-18/src/lib.rs index c9d1f52..7dd7c91 100755 --- a/listings/ch12-an-io-project/listing-12-18/src/lib.rs +++ b/listings/ch12-an-io-project/listing-12-18/src/lib.rs @@ -29,7 +29,7 @@ pub fn run(config: Config) -> Result<(), Box> { pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { - // do something with line + // 对文本行进行操作 } } } diff --git a/src/ch12-01-accepting-command-line-arguments.md b/src/ch12-01-accepting-command-line-arguments.md index b7bb3f7..9e5eb1a 100644 --- a/src/ch12-01-accepting-command-line-arguments.md +++ b/src/ch12-01-accepting-command-line-arguments.md @@ -2,11 +2,11 @@ > [ch12-01-accepting-command-line-arguments.md](https://github.com/rust-lang/book/blob/main/src/ch12-01-accepting-command-line-arguments.md) >
-> commit c084bdd9ee328e7e774df19882ccc139532e53d8 +> commit 0f87daf683ae3de3cb725faecb11b7e7e89f0e5a 一如既往使用 `cargo new` 新建一个项目,我们称之为 `minigrep` 以便与可能已经安装在系统上的 `grep` 工具相区别: -```text +```console $ cargo new minigrep Created binary (application) `minigrep` project $ cd minigrep @@ -14,7 +14,7 @@ $ cd minigrep 第一个任务是让 `minigrep` 能够接受两个命令行参数:文件名和要搜索的字符串。也就是说我们希望能够使用 `cargo run`、要搜索的字符串和被搜索的文件的路径来运行程序,像这样: -```text +```console $ cargo run searchstring example-filename.txt ``` @@ -29,12 +29,7 @@ $ cargo run searchstring example-filename.txt 文件名: src/main.rs ```rust -use std::env; - -fn main() { - let args: Vec = env::args().collect(); - println!("{:?}", args); -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-01/src/main.rs}} ``` 示例 12-1:将命令行参数收集到一个 vector 中并打印出来 @@ -49,14 +44,12 @@ fn main() { 最后,我们使用调试格式 `:?` 打印出 vector。让我们尝试分别用两种方式(不包含参数和包含参数)运行代码: -```text -$ cargo run ---snip-- -["target/debug/minigrep"] +```console +{{#include ../listings/ch12-an-io-project/listing-12-01/output.txt}} +``` -$ cargo run needle haystack ---snip-- -["target/debug/minigrep", "needle", "haystack"] +```console +{{#include ../listings/ch12-an-io-project/output-only-01-with-args/output.txt}} ``` 注意 vector 的第一个值是 `"target/debug/minigrep"`,它是我们二进制文件的名称。这与 C 中的参数列表的行为相匹配,让程序使用在执行时调用它们的名称。如果要在消息中打印它或者根据用于调用程序的命令行别名更改程序的行为,通常可以方便地访问程序名称,不过考虑到本章的目的,我们将忽略它并只保存所需的两个参数。 @@ -67,18 +60,8 @@ $ cargo run needle haystack 文件名: src/main.rs -```rust,should_panic -use std::env; - -fn main() { - let args: Vec = env::args().collect(); - - let query = &args[1]; - let filename = &args[2]; - - println!("Searching for {}", query); - println!("In file {}", filename); -} +```rust,should_panic,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-02/src/main.rs}} ``` 示例 12-2:创建变量来存放查询参数和文件名参数 @@ -87,16 +70,11 @@ fn main() { 我们将临时打印出这些变量的值来证明代码如我们期望的那样工作。使用参数 `test` 和 `sample.txt` 再次运行这个程序: -```text -$ cargo run test sample.txt - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep test sample.txt` -Searching for test -In file sample.txt +```console +{{#include ../listings/ch12-an-io-project/listing-12-02/output.txt}} ``` 好的,它可以工作!我们将所需的参数值保存进了对应的变量中。之后会增加一些错误处理来应对类似用户没有提供参数的情况,不过现在我们将忽略他们并开始增加读取文件功能。 [ch13]: ch13-00-functional-features.html -[ch7-idiomatic-use]: ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#creating-idiomatic-use-paths +[ch7-idiomatic-use]: ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#创建惯用的-use-路径 diff --git a/src/ch12-02-reading-a-file.md b/src/ch12-02-reading-a-file.md index c29b271..cef8f39 100644 --- a/src/ch12-02-reading-a-file.md +++ b/src/ch12-02-reading-a-file.md @@ -2,22 +2,14 @@ > [ch12-02-reading-a-file.md](https://github.com/rust-lang/book/blob/main/src/ch12-02-reading-a-file.md) >
-> commit 76df60bccead5f3de96db23d97b69597cd8a2b82 +> commit 0f87daf683ae3de3cb725faecb11b7e7e89f0e5a 现在我们要增加读取由 `filename` 命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:用来确保 `minigrep` 正常工作的最好的文件是拥有多行少量文本且有一些重复单词的文件。示例 12-3 是一首艾米莉·狄金森(Emily Dickinson)的诗,它正适合这个工作!在项目根目录创建一个文件 `poem.txt`,并输入诗 "I'm nobody! Who are you?": 文件名: poem.txt ```text -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 frog -To tell your name the livelong day -To an admiring bog! +{{#include ../listings/ch12-an-io-project/listing-12-03/poem.txt}} ``` 示例 12-3:艾米莉·狄金森的诗 “I’m nobody! Who are you?”,一个好的测试用例 @@ -26,25 +18,8 @@ To an admiring bog! 文件名: src/main.rs -```rust,should_panic -use std::env; -use std::fs; - -fn main() { -# let args: Vec = env::args().collect(); -# -# let query = &args[1]; -# let filename = &args[2]; -# -# println!("Searching for {}", query); - // --snip-- - println!("In file {}", filename); - - let contents = fs::read_to_string(filename) - .expect("Something went wrong reading the file"); - - println!("With text:\n{}", contents); -} +```rust,should_panic,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/src/main.rs:here}} ``` 示例 12-4:读取第二个参数所指定的文件内容 @@ -57,23 +32,8 @@ fn main() { 尝试运行这些代码,随意指定一个字符串作为第一个命令行参数(因为还未实现搜索功能的部分)而将 *poem.txt* 文件将作为第二个参数: -```text -$ cargo run the poem.txt - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep the poem.txt` -Searching for the -In file poem.txt -With text: -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 frog -To tell your name the livelong day -To an admiring bog! +```console +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-04/output.txt}} ``` 好的!代码读取并打印出了文件的内容。虽然它还有一些瑕疵:`main` 函数有着多个职能,通常函数只负责一个功能的话会更简洁并易于维护。另一个问题是没有尽可能的处理错误。虽然我们的程序还很小,这些瑕疵并不是什么大问题,不过随着程序功能的丰富,将会越来越难以用简单的方法修复他们。在开发程序时,及早开始重构是一个最佳实践,因为重构少量代码时要容易的多,所以让我们现在就开始吧。 diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index 19b49c4..50af066 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -2,7 +2,7 @@ > [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/main/src/ch12-03-improving-error-handling-and-modularity.md) >
-> commit 426f3e4ec17e539ae9905ba559411169d303a031 +> commit c8a9ac9cee7923422b2eceebf0375363440dbfc1 为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。 @@ -40,20 +40,7 @@ 文件名: src/main.rs ```rust,ignore -fn main() { - let args: Vec = 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) -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-05/src/main.rs:here}} ``` 示例 12-5:从 `main` 中提取出 `parse_config` 函数 @@ -74,35 +61,8 @@ fn parse_config(args: &[String]) -> (&str, &str) { 文件名: src/main.rs -```rust,should_panic -# use std::env; -# use std::fs; -# -fn main() { - let args: Vec = env::args().collect(); - - let config = parse_config(&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"); - - // --snip-- -} - -struct Config { - query: String, - filename: String, -} - -fn parse_config(args: &[String]) -> Config { - let query = args[1].clone(); - let filename = args[2].clone(); - - Config { query, filename } -} +```rust,should_panic,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-06/src/main.rs:here}} ``` 示例 12-6:重构 `parse_config` 返回一个 `Config` 结构体实例 @@ -128,32 +88,8 @@ fn parse_config(args: &[String]) -> Config { 文件名: src/main.rs -```rust,should_panic -# use std::env; -# -fn main() { - let args: Vec = env::args().collect(); - - let config = Config::new(&args); - - // --snip-- -} - -# struct Config { -# query: String, -# filename: String, -# } -# -// --snip-- - -impl Config { - fn new(args: &[String]) -> Config { - let query = args[1].clone(); - let filename = args[2].clone(); - - Config { query, filename } - } -} +```rust,should_panic,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-07/src/main.rs:here}} ``` 示例 12-7:将 `parse_config` 变为 `Config::new` @@ -164,14 +100,8 @@ impl Config { 现在我们开始修复错误处理。回忆一下之前提到过如果 `args` vector 包含少于 3 个项并尝试访问 vector 中索引 `1` 或索引 `2` 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样: -```text -$ cargo run - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep` -thread 'main' panicked at 'index out of bounds: the len is 1 -but the index is 1', src/main.rs:25:21 -note: Run with `RUST_BACKTRACE=1` for a backtrace. +```console +{{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}} ``` `index out of bounds: the len is 1 but the index is 1` 是一个针对程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。 @@ -183,27 +113,17 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. 文件名: src/main.rs ```rust,ignore -// --snip-- -fn new(args: &[String]) -> Config { - if args.len() < 3 { - panic!("not enough arguments"); - } - // --snip-- +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-08/src/main.rs:here}} ``` 示例 12-8:增加一个参数数量检查 -这类似于 [示例 9-10 中的 `Guess::new` 函数][ch9-custom-types],那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 +这类似于 [示例 9-13 中的 `Guess::new` 函数][ch9-custom-types],那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 `3`,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: -```text -$ cargo run - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep` -thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 -note: Run with `RUST_BACKTRACE=1` for a backtrace. +```console +{{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}} ``` 这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如 [第九章][ch9-error-guidelines] 所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术 —— 返回一个可以表明成功或错误的 [`Result`][ch9-result]。 @@ -216,19 +136,8 @@ note: Run with `RUST_BACKTRACE=1` for a backtrace. 文件名: src/main.rs -```rust,ignore -impl Config { - fn new(args: &[String]) -> Result { - if args.len() < 3 { - return Err("not enough arguments"); - } - - let query = args[1].clone(); - let filename = args[2].clone(); - - Ok(Config { query, filename }) - } -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-09/src/main.rs:here}} ``` 示例 12-9:从 `Config::new` 中返回 `Result` @@ -246,31 +155,17 @@ impl Config { 文件名: src/main.rs ```rust,ignore -use std::process; - -fn main() { - let args: Vec = env::args().collect(); - - let config = Config::new(&args).unwrap_or_else(|err| { - println!("Problem parsing arguments: {}", err); - process::exit(1); - }); - - // --snip-- +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-10/src/main.rs:here}} ``` 示例 12-10:如果新建 `Config` 失败则使用错误码退出 -在上面的示例中,使用了一个之前没有涉及到的方法:`unwrap_or_else`,它定义于标准库的 `Result` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第十三章][ch13] 会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。 +在上面的示例中,使用了一个之前没有详细说明的方法:`unwrap_or_else`,它定义于标准库的 `Result` 上。使用 `unwrap_or_else` 可以进行一些自定义的非 `panic!` 的错误处理。当 `Result` 是 `Ok` 时,这个方法的行为类似于 `unwrap`:它返回 `Ok` 内部封装的值。然而,当其值是 `Err` 时,该方法会调用一个 **闭包**(*closure*),也就是一个我们定义的作为参数传递给 `unwrap_or_else` 的匿名函数。[第十三章][ch13] 会更详细的介绍闭包。现在你需要理解的是 `unwrap_or_else` 会将 `Err` 的内部值,也就是示例 12-9 中增加的 `not enough arguments` 静态字符串的情况,传递给闭包中位于两道竖线间的参数 `err`。闭包中的代码在其运行时可以使用这个 `err` 值。 我们新增了一个 `use` 行来从标准库中导入 `process`。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 `err` 值,接着调用了 `std::process::exit`。`process::exit` 会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 `panic!` 的错误处理,除了不会再得到所有的额外输出了。让我们试试: -```text -$ cargo run - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs - Running `target/debug/minigrep` -Problem parsing arguments: not enough arguments +```console +{{#include ../listings/ch12-an-io-project/listing-12-10/output.txt}} ``` 非常好!现在输出对于用户来说就友好多了。 @@ -284,23 +179,7 @@ Problem parsing arguments: not enough arguments 文件名: src/main.rs ```rust,ignore -fn main() { - // --snip-- - - println!("Searching for {}", config.query); - println!("In file {}", config.filename); - - run(config); -} - -fn run(config: Config) { - let contents = fs::read_to_string(config.filename) - .expect("Something went wrong reading the file"); - - println!("With text:\n{}", contents); -} - -// --snip-- +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-11/src/main.rs:here}} ``` 示例 12-11:提取 `run` 函数来包含剩余的程序逻辑 @@ -314,17 +193,7 @@ fn run(config: Config) { 文件名: src/main.rs ```rust,ignore -use std::error::Error; - -// --snip-- - -fn run(config: Config) -> Result<(), Box> { - let contents = fs::read_to_string(config.filename)?; - - println!("With text:\n{}", contents); - - Ok(()) -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-12/src/main.rs:here}} ``` 示例 12-12:修改 `run` 函数返回 `Result` @@ -339,15 +208,8 @@ fn run(config: Config) -> Result<(), Box> { 上述代码能够编译,不过会有一个警告: -```text -warning: unused `std::result::Result` that must be used - --> src/main.rs:17:5 - | -17 | run(config); - | ^^^^^^^^^^^^ - | - = note: #[warn(unused_must_use)] on by default - = note: this `Result` may be an `Err` variant, which should be handled +```console +{{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}} ``` Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。但我们却没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正这个问题。 @@ -359,18 +221,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在 文件名: src/main.rs ```rust,ignore -fn main() { - // --snip-- - - println!("Searching for {}", config.query); - println!("In file {}", config.filename); - - if let Err(e) = run(config) { - println!("Application error: {}", e); - - process::exit(1); - } -} +{{#rustdoc_include ../listings/ch12-an-io-project/no-listing-01-handling-errors-in-main/src/main.rs:here}} ``` 我们使用 `if let` 来检查 `run` 是否返回一个 `Err` 值,不同于 `unwrap_or_else`,并在出错时调用 `process::exit(1)`。`run` 并不返回像 `Config::new` 返回的 `Config` 实例那样需要 `unwrap` 的值。因为 `run` 在成功时返回 `()`,而我们只关心检测错误,所以并不需要 `unwrap_or_else` 来返回未封装的值,因为它只会是 `()`。 @@ -392,24 +243,8 @@ fn main() { 文件名: src/lib.rs -```rust,ignore -use std::error::Error; -use std::fs; - -pub struct Config { - pub query: String, - pub filename: String, -} - -impl Config { - pub fn new(args: &[String]) -> Result { - // --snip-- - } -} - -pub fn run(config: Config) -> Result<(), Box> { - // --snip-- -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-13/src/lib.rs:here}} ``` 示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs* @@ -421,17 +256,7 @@ pub fn run(config: Config) -> Result<(), Box> { Filename: src/main.rs ```rust,ignore -use std::env; -use std::process; - -use minigrep::Config; - -fn main() { - // --snip-- - if let Err(e) = minigrep::run(config) { - // --snip-- - } -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-14/src/main.rs:here}} ``` 示例 12-14:将 `minigrep` crate 引入 *src/main.rs* 的作用域中 @@ -442,7 +267,6 @@ fn main() { 让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,这些工作在新代码中非常容易实现,那就是:编写测试! -[the-static-lifetime]: ch10-03-lifetime-syntax.html#the-static-lifetime [ch13]: ch13-00-functional-features.html [ch9-custom-types]: ch09-03-to-panic-or-not-to-panic.html#creating-custom-types-for-validation [ch9-error-guidelines]: ch09-03-to-panic-or-not-to-panic.html#guidelines-for-error-handling diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 31b98f8..72f201f 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -2,9 +2,9 @@ > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/main/src/ch12-04-testing-the-librarys-functionality.md) >
-> commit 0ca4b88f75f8579de87adc2ad36d340709f5ccad +> commit 04170d1feee2a47525b39f1edce77ba615ca9cdf -现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new` 和 `run` 函数的功能编写一些测试。 +现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。 在这一部分,我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤: @@ -23,43 +23,20 @@ 文件名: src/lib.rs -```rust -# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { -# vec![] -# } -# -#[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) - ); - } -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-15/src/lib.rs:here}} ``` 示例 12-15:创建一个我们期望的 `search` 函数的失败测试 -这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。我们断言 `search` 函数的返回值只包含期望的那一行。 +这里选择使用 `"duct"` 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 `"duct"`。(注意双引号之后的反斜杠,这告诉 Rust 不要在字符串字面值内容的开头加入换行符)我们断言 `search` 函数的返回值只包含期望的那一行。 我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译:`search` 函数还不存在呢!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 文件名: src/lib.rs -```rust -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - vec![] -} +```rust,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-16/src/lib.rs:here}} ``` 示例 12-16:刚好足够使测试通过编译的 `search` 函数定义 @@ -70,16 +47,8 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 如果尝试不用生命周期编译的话,我们将得到如下错误: -```text -error[E0106]: missing lifetime specifier - --> src/lib.rs:5:51 - | -5 | pub fn search(query: &str, contents: &str) -> Vec<&str> { - | ^ expected lifetime -parameter - | - = help: this function's return type contains a borrowed value, but the - signature does not say whether it is borrowed from `query` or `contents` +```console +{{#include ../listings/ch12-an-io-project/output-only-02-missing-lifetimes/output.txt}} ``` Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。 @@ -88,32 +57,8 @@ Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。 现在运行测试: -```text -$ cargo test - Compiling minigrep v0.1.0 (file:///projects/minigrep) ---warnings-- - Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs - Running target/debug/deps/minigrep-abcabcabc - -running 1 test -test tests::one_result ... FAILED - -failures: - ----- tests::one_result stdout ---- - thread 'tests::one_result' panicked at 'assertion failed: `(left == -right)` -left: `["safe, fast, productive."]`, -right: `[]`)', src/lib.rs:48:8 -note: Run with `RUST_BACKTRACE=1` for a backtrace. - - -failures: - tests::one_result - -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out - -error: test failed, to rerun pass '--lib' +```console +{{#include ../listings/ch12-an-io-project/listing-12-16/output.txt}} ``` 好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧! @@ -136,12 +81,8 @@ Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被 文件名: src/lib.rs -```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - for line in contents.lines() { - // do something with line - } -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-17/src/lib.rs:here}} ``` 示例 12-17:遍历 `contents` 的每一行 @@ -154,14 +95,8 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 文件名: src/lib.rs -```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - for line in contents.lines() { - if line.contains(query) { - // do something with line - } - } -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-18/src/lib.rs:here}} ``` 示例 12-18:增加检查文本行是否包含 `query` 中字符串的功能 @@ -173,30 +108,15 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 文件名: src/lib.rs ```rust,ignore -pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - let mut results = Vec::new(); - - for line in contents.lines() { - if line.contains(query) { - results.push(line); - } - } - - results -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:here}} ``` 示例 12-19:储存匹配的行以便可以返回他们 现在 `search` 函数应该返回只包含 `query` 的那些行,而测试应该会通过。让我们运行测试: -```text -$ cargo test ---snip-- -running 1 test -test tests::one_result ... ok - -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +```console +{{#include ../listings/ch12-an-io-project/listing-12-19/output.txt}} ``` 测试通过了,它可以工作了! @@ -210,55 +130,37 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 文件名: src/lib.rs ```rust,ignore -pub fn run(config: Config) -> Result<(), Box> { - let contents = fs::read_to_string(config.filename)?; - - for line in search(&config.query, &contents) { - println!("{}", line); - } - - Ok(()) -} +{{#rustdoc_include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/src/lib.rs:here}} ``` 这里仍然使用了 `for` 循环获取了 `search` 返回的每一行并打印出来。 现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 “frog”: -```text -$ cargo run frog poem.txt - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs - Running `target/debug/minigrep frog poem.txt` -How public, like a frog +```console +{{#include ../listings/ch12-an-io-project/no-listing-02-using-search-in-run/output.txt}} ``` 好的!现在试试一个会匹配多行的单词,比如 “body”: -```text -$ cargo run body poem.txt - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep body poem.txt` -I’m nobody! Who are you? -Are you nobody, too? -How dreary to be somebody! +```console +{{#include ../listings/ch12-an-io-project/output-only-03-multiple-matches/output.txt}} ``` 最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization": -```text -$ cargo run monomorphization poem.txt - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep monomorphization poem.txt` +```console +{{#include ../listings/ch12-an-io-project/output-only-04-no-matches/output.txt}} ``` + 非常好!我们创建了一个属于自己的迷你版经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 为了使这个项目更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。 [validating-references-with-lifetimes]: -ch10-03-lifetime-syntax.html#validating-references-with-lifetimes +ch10-03-lifetime-syntax.html#生命周期与引用有效性 [ch11-anatomy]: ch11-01-writing-tests.html#the-anatomy-of-a-test-function [ch10-lifetimes]: ch10-03-lifetime-syntax.html -[ch3-iter]: ch03-05-control-flow.html#looping-through-a-collection-with-for +[ch3-iter]: ch03-05-control-flow.html#使用-for-遍历集合 [ch13-iterators]: ch13-02-iterators.html diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index a6f2503..c50498a 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -2,7 +2,7 @@ > [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/main/src/ch12-05-working-with-environment-variables.md) >
-> commit f617d58c1a88dd2912739a041fd4725d127bf9fb +> commit 9c0fa2714859738ff73cbbb829592e4c037d7e46 我们将增加一个额外的功能来改进 `minigrep`:用户可以通过设置环境变量来设置搜索是否是大小写敏感的 。当然,我们也可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过在这里我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 @@ -12,41 +12,8 @@ 文件名: src/lib.rs -```rust -#[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) - ); - } -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-20/src/lib.rs:here}} ``` 示例 12-20:为准备添加的大小写不敏感函数新增失败测试 @@ -61,24 +28,13 @@ Trust me."; 文件名: src/lib.rs -```rust -pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { - let query = query.to_lowercase(); - let mut results = Vec::new(); - - for line in contents.lines() { - if line.to_lowercase().contains(&query) { - results.push(line); - } - } - - results -} +```rust,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-21/src/lib.rs:here}} ``` 示例 12-21:定义 `search_case_insensitive` 函数,它在比较查询和每一行之前将他们都转换为小写 -首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。 +首先我们将 `query` 字符串转换为小写,并将其覆盖到同名的变量中。对查询字符串调用 `to_lowercase` 是必需的,这样不管用户的查询是 `"rust"`、`"RUST"`、`"Rust"` 或者 `"rUsT"`,我们都将其当作 `"rust"` 处理并对大小写不敏感。虽然 `to_lowercase` 可以处理基本的 Unicode,但它不是 100% 准确。如果编写真实的程序的话,我们还需多做一些工作,不过这一部分是关于环境变量而不是 Unicode 的,所以这样就够了。 注意 `query` 现在是一个 `String` 而不是字符串 slice,因为调用 `to_lowercase` 是在创建新数据,而不是引用现有数据。如果查询字符串是 `"rUsT"`,这个字符串 slice 并不包含可供我们使用的小写的 `u` 或 `t`,所以必需分配一个包含 `"rust"` 的新 `String`。现在当我们将 `query` 作为一个参数传递给 `contains` 方法时,需要增加一个 & 因为 `contains` 的签名被定义为获取一个字符串 slice。 @@ -86,64 +42,24 @@ pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a st 让我们看看这个实现能否通过测试: -```text -running 2 tests -test tests::case_insensitive ... ok -test tests::case_sensitive ... ok - -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +```console +{{#include ../listings/ch12-an-io-project/listing-12-21/output.txt}} ``` 好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。增加这些字段会导致编译错误,因为我们还没有在任何地方初始化这些字段: 文件名: src/lib.rs -```rust -pub struct Config { - pub query: String, - pub filename: String, - pub case_sensitive: bool, -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:here}} ``` 这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示。注意这还不能编译: 文件名: src/lib.rs -```rust -# use std::error::Error; -# use std::fs::{self, File}; -# use std::io::prelude::*; -# -# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { -# vec![] -# } -# -# pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { -# vec![] -# } -# -# pub struct Config { -# query: String, -# filename: String, -# case_sensitive: bool, -# } -# -pub fn run(config: Config) -> Result<(), Box> { - let contents = fs::read_to_string(config.filename)?; - - let results = if config.case_sensitive { - search(&config.query, &contents) - } else { - search_case_insensitive(&config.query, &contents) - }; - - for line in results { - println!("{}", line); - } - - Ok(()) -} +```rust,ignore,does_not_compile +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-22/src/lib.rs:there}} ``` 示例 12-22:根据 `config.case_sensitive` 的值调用 `search` 或 `search_case_insensitive` @@ -152,30 +68,8 @@ pub fn run(config: Config) -> Result<(), Box> { 文件名: src/lib.rs -```rust -use std::env; -# struct Config { -# query: String, -# filename: String, -# case_sensitive: bool, -# } - -// --snip-- - -impl Config { - pub fn new(args: &[String]) -> Result { - if args.len() < 3 { - return Err("not enough arguments"); - } - - let query = args[1].clone(); - let filename = args[2].clone(); - - let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); - - Ok(Config { query, filename, case_sensitive }) - } -} +```rust,noplayground +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-23/src/lib.rs:here}} ``` 示例 12-23:检查叫做 `CASE_INSENSITIVE` 的环境变量 @@ -188,29 +82,23 @@ impl Config { 让我们试一试吧!首先不设置环境变量并使用查询 `to` 运行程序,这应该会匹配任何全小写的单词 “to” 的行: -```text -$ cargo run to poem.txt - Compiling minigrep v0.1.0 (file:///projects/minigrep) - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep to poem.txt` -Are you nobody, too? -How dreary to be somebody! +```console +{{#include ../listings/ch12-an-io-project/listing-12-23/output.txt}} ``` 看起来程序仍然能够工作!现在将 `CASE_INSENSITIVE` 设置为 `1` 并仍使用相同的查询 `to`。 -如果你使用 PowerShell,则需要用两个命令来设置环境变量并运行程序: +如果你使用 PowerShell,则需要用两个命令来分别设置环境变量并运行程序: -```text -$ $env:CASE_INSENSITIVE=1 -$ cargo run to poem.txt +```console +PS> $Env:CASE_INSENSITIVE=1; cargo run to poem.txt ``` 这回应该得到包含可能有大写字母的 “to” 的行: -```text +```console $ CASE_INSENSITIVE=1 cargo run to poem.txt - Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs + Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody! diff --git a/src/ch12-06-writing-to-stderr-instead-of-stdout.md b/src/ch12-06-writing-to-stderr-instead-of-stdout.md index f182a18..596e57c 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -2,11 +2,11 @@ > [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/main/src/ch12-06-writing-to-stderr-instead-of-stdout.md) >
-> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f +> commit c5c12e911b20fac20eefc511f6fe8d432a8e5ec2 -目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应一般信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 +目前为止,我们将所有的输出都通过 `println!` 写到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应一般信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 -但是 `println!` 函数只能够打印到标准输出,所以我们必须使用其他方法来打印到标准错误。 +但是 `println!` 宏只能够打印到标准输出,所以我们必须使用其他方法来打印到标准错误。 ### 检查错误应该写入何处 @@ -16,7 +16,7 @@ 我们通过 `>` 和文件名 *output.txt* 来运行程序,我们期望重定向标准输出流到该文件中。在这里,我们没有传递任何参数,所以会产生一个错误: -```text +```console $ cargo run > output.txt ``` @@ -35,27 +35,14 @@ Problem parsing arguments: not enough arguments 文件名: src/main.rs ```rust,ignore -fn main() { - let args: Vec = env::args().collect(); - - 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); - - process::exit(1); - } -} +{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-24/src/main.rs:here}} ``` 示例 12-24:使用 `eprintln!` 将错误信息写入标准错误而不是标准输出 将 `println!` 改为 `eprintln!` 之后,让我们再次尝试用同样的方式运行程序,不使用任何参数并通过 `>` 重定向标准输出: -```text +```console $ cargo run > output.txt Problem parsing arguments: not enough arguments ``` @@ -64,7 +51,7 @@ Problem parsing arguments: not enough arguments 如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样: -```text +```console $ cargo run to poem.txt > output.txt ```