DEV Community

Cover image for [Rust Guide]12.8. Writing Error Messages to Standard Error
SomeB1oody
SomeB1oody

Posted on

[Rust Guide]12.8. Writing Error Messages to Standard Error

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)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then try a normal run with arguments:

$ cargo run -- to poem.txt > output.txt
Enter fullscreen mode Exit fullscreen mode

The output is redirected to output.txt. Open it:

Are you nobody, too?
How dreary to be somebody!
Enter fullscreen mode Exit fullscreen mode

That is the result we want: errors are printed directly in the terminal, while normal output is redirected into the file.

Top comments (0)