12.8.0 Before We Begin
Chapter 12 builds a sample project: a command-line program. The program is grep (Global Regular Expression Print), a tool for global regular-expression searching and output. Its function is to search for specified text in a specified file.
This project is divided into these steps:
- Receiving command-line arguments
- Reading files
- Refactoring: improving modules and error handling
- Using TDD (test-driven development) to develop library functionality
- Using environment variables
- Writing error messages to standard error instead of standard output (this article)
If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
12.8.1 Review
Here is all the code written up to the previous article.
lib.rs:
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 Standard Output vs. Standard Error
The current code prints all information, including error messages, to the terminal. Most terminals provide two output streams: standard output (stdout) and standard error (stderr).
General information should go to standard output, while error messages should go to standard error. The advantage of this separation is that normal output can be redirected into a file while error messages still appear on the screen.
The println! macro can only print to standard output. The eprintln! macro can print to standard error.
Using the current code, run this command in the terminal:
cargo run > output.txt
This redirects output to output.txt, but the command does not include any arguments, so the program should error. Because the error messages are also written to standard output, they end up in output.txt.
A better approach is to print error messages to standard error, which keeps standard output clean and separate from errors.
12.8.3 Modifying the Code
Changing the code so that error messages go to standard error is quite simple. We only need to change all error printing from println! to eprintln!. Because all error handling is in main.rs, we only need a small change there, and lib.rs does not need to be modified at all:
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);
}
}
Now run the previous command again. It still has no arguments, so the program errors, but this time it will not put the error message into output.txt; instead, it will print directly in the terminal:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
Then try a normal run with arguments:
$ cargo run -- to poem.txt > output.txt
The output is redirected to output.txt. Open it:
Are you nobody, too?
How dreary to be somebody!
That is the result we want: errors are printed directly in the terminal, while normal output is redirected into the file.
Top comments (0)