这次创建了新的工程minigrep.
此时,我们正在使用宏println!将所有输出写入终端。在大多数终端中,有两种输出:一般信息的标准输出(stdout)和错误消息的标准错误(stderr)。这种区别使用户能够选择将程序的成功输出指向文件,但仍将错误信息打印到屏幕上。
println!宏只能打印到标准输出,所以我们必须使用其他东西打印到标准错误。
首先,让我们观察minigrep打印的内容当前是如何写入标准输出的,包括我们希望改为写入标准错误的任何错误消息。我们将通过将标准输出流重定向到一个文件来实现这一点,同时故意造成一个错误。我们不会重定向标准错误流,因此发送到标准错误的任何内容都将继续显示在屏幕上。
命令行程序应该将错误消息发送到标准错误流,这样即使我们将标准输出流重定向到文件,我们仍然可以在屏幕上看到错误消息。我们的程序目前表现不佳:我们将看到它将错误消息输出保存到一个文件中!
为了演示这种行为,我们将使用》和文件路径output.txt运行程序,我们希望将标准输出流重定向到该文件路径。我们不会传递任何参数,这会导致一个错误:
$ cargo run > output.txt
> 语法告诉系统将标准输出的内容写入output.txt而不是屏幕。我们没有看到我们期望的打印到屏幕上的错误消息,所以这意味着它一定在文件中结束了。以下是output.txt包含的内容:
Problem parsing arguments: not enough arguments
是的,我们的错误信息被打印到标准输出。将这样的错误信息打印到标准错误中会更有用,这样只有成功运行的数据才会出现在文件中。我们会改变的。
我们将使用示例12-24中的代码来改变错误信息的打印方式。由于我们在本章前面所做的重构,所有打印错误信息的代码都在一个函数main中。标准库提供了eprintln!打印到标准错误流的宏,所以让我们更改我们调用println!的两个位置!要打印错误,请使用eprintln!相反。
文件名:src/main.rs
- fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = cfg::Config::build(&args).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- if let Err(e) = cfg::run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
- }
示例12-24:使用eprintln!将错误消息写入标准错误而不是标准输出
现在让我们以同样的方式再次运行程序,没有任何参数,并且使用>重定向标准输出:
- $ cargo run > output.txt
- Problem parsing arguments: not enough arguments
现在我们在屏幕上看到错误,output.txt不包含任何内容,这是我们期望命令行程序的行为。
让我们使用不会导致错误但仍将标准输出重定向到文件的参数再次运行程序,如下所示:
$ cargo run -- to poem.txt > output.txt
我们不会在终端上看到任何输出,output.txt将包含我们的结果:
文件名:output.txt
- Are you nobody, too?
- How dreary to be somebody!
运行结果:
这表明我们现在使用标准输出进行成功输出,并根据需要使用标准错误进行错误输出。
文件名: main.rs
- use std::env;
- use std::process;
- use minigrep::cfg;
-
-
- fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = cfg::Config::build(&args).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- if let Err(e) = cfg::run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
- }
文件名:lib.rs
- pub mod cfg {
- use std::error::Error;
- use std::env;
- use std::fs;
-
- pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
- }
-
- impl Config {
- pub fn build(args: &[String]) -> Result
'static str> { - if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- let ignore_case = env::var("IGNORE_CASE").is_ok();
-
- Ok(Config {
- query,
- file_path,
- ignore_case,
- })
- }
- }
-
-
- #[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)
- );
- }
- }
-
- 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
- }
-
-
- 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
- }
-
- pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
- }
- }
本章概述了到目前为止您所学的一些主要概念,并介绍了如何在Rust中执行常见的I/O操作。通过使用命令行参数、文件、环境变量和eprintln!宏处理打印错误,现在您可以准备编写命令行应用程序了。结合前几章的概念,您的代码将组织良好,在适当的数据结构中有效地存储数据,很好地处理错误,并得到良好的测试。
接下来,我们将探索一些受函数式语言影响的Rust特性:闭包和迭代器。