12.8 将错误信息写入到标准错误
12.8.0. 写在正文之前
第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。
这个项目分为这么几步:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量
- 将错误信息写入标准错误而不是标准输出(本文)
12.8.1. 回顾
以下是截止到上一篇文章为止所写出的全部代码。
lib.rs:
#![allow(unused)]
fn main() {
use std::error::Error;
use std::fs;
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");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = std::env::var("IGNORE_CASE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
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(())
}
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 mut results = Vec::new();
let query = query.to_lowercase();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[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)
);
}
}
}
main.rs:
use std::env;
use std::process;
use minigrep::Config;
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);
});
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
12.8.2. 标准输出 vs. 标准错误
这个代码目前把所有的信息(包括错误信息)都打印到终端上了,而大多数的终端它都提供两种输出:标准输出(stdout) 和 标准错误(stderr)。
一般的信息应该输出到标准输出里,而错误信息应该输出到标准错误里。 这种区分的好处在于使正常的输出重定向到文件里面,而错误信息可以在屏幕上打印。
println!这个宏只能将信息打印到标准输出里。而eprintln!这个宏可以把信息输出到标准错误里。
我们使用目前的代码,在终端中执行这个命令:
cargo run > output.txt
也就是把输出重定向到output.txt里,但是这个指令没有带参数,也就是说程序应该会报错,但由于我们把错误信息也写在标准输出里,所以说报错的信息被写在output.txt里了。
更好的做法是将错误信息打印到标准错误里,这样就可以让标准输出的内容保持整洁,不与报错信息混在一起。
12.8.3. 修改代码
将错误信息打印到标准错误里的代码修改还算简单,我们只需要把所有的报错信息从用println!打印改为用eprintln!打印即可。因为所有的报错处理都在main.rs里,所以我们只需要对main.rs稍作修改即可,lib.rs就完全不需要修改:
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = 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);
}
}
然后我们再执行之前那个命令。那个指令没有带参数,所以程序会报错,但这次它不会把报错信息放在output.txt里,而是会直接在终端中打印出来:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
然后再试一下正常带参数的:
$ cargo run -- to poem.txt > output.txt
输出内容被重定向到output.txt里了,打开它:
Are you nobody, too?
How dreary to be somebody!
这就是我们想要的结果:错误直接在终端打印,而正常输出在重定向的文件里。