12.4.0 Before We Begin
In Chapter 12, we will build a real project: a command-line program. This program is a grep (Global Regular Expression Print), a tool for global regular-expression search and output. Its job is to search for the specified text in the specified file.
This project has several steps:
- Receive command-line arguments
- Read files
- Refactor: improve modules and error handling (this article)
- Use TDD (test-driven development) to develop library functionality
- Use environment variables
- Write error messages to standard error instead of standard output
If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
12.4.1 Review
In the previous section, to improve modularity, we created a struct for the variables and moved the argument-parsing function into a method on that struct. Here is all the code written up to the previous article:
use std::env;
use std::fs;
struct Config {
query: String,
filename: String,
}
fn main() {
let args:Vec<String> = env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong while reading the file");
println!("With text:\n{}", contents);
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config {
query,
filename,
}
}
}
12.4.2 Unexpected Input
The program works correctly only when the user provides valid input. Let’s try running it without arguments:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
It reports Index out of bounds. As the programmer, we know this is because there were not enough arguments, so the program went out of bounds when using an index to fetch them. But a user cannot understand this error message, so they cannot correct the mistake.
What this article will do is make the program’s error messages easier to understand.
12.4.3 Specifying Error Messages
The way to help users understand the error is to provide our own error message. In the previous example, the panic happened when Config::new tried to access an out-of-bounds index, so let’s modify that part:
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Config {
query,
filename,
}
}
}
If args contains fewer than three elements, panic and print "Not enough arguments" to tell the user that too few arguments were provided.
Try running it without arguments again:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This error message is much better than the previous one.
However, some extra information is still shown, such as thread 'main' panicked at src/main.rs:26:13: and note: run with RUST_BACKTRACE=1 environment variable to display a backtrace. Those are for programmers, not for users, so they should be removed too.
12.4.4 Using the Result Type
panic! is appropriate when the program itself has a bug. But here, the problem is that the program was used incorrectly because there were too few arguments. For this kind of problem, using Result to propagate the error is the best choice (see 9.2. Result Enum and Recoverable Errors Pt. 1 and Pt. 2 for details):
impl Config {
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();
Ok(Config { query, filename})
}
}
- Error information must be wrapped in
Err, and successful return values must be wrapped inOk. - The
Okvariant ofResultreturns aConfiginstance, whileErrreturns an&strstring literal. However, the compiler does not know where that&strcomes from or how long its lifetime is, so we need a lifetime annotation. We want it to remain valid for the entire program run, so we write it as&'static str, the static lifetime.
Since the return type of new has changed, the code in main that receives its value must also change:
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
The unwrap_or_else method accepts a Result. If it is Ok, it returns the value inside Ok, similar to unwrap. If it is Err, the method calls a closure.
A closure is an anonymous function that we define and pass as an argument to unwrap_or_else. Its syntax uses two pipes ||, with a variable name in the middle as the parameter. Here it is err, which can be used inside the closure body, such as when printing the error.
Then we use process::exit from the standard library. Remember to import it first with use std::process;. Calling exit terminates the program immediately, and its argument, 1 in the example, becomes the program’s exit status code. This means that after println!("Problem parsing arguments: {}", err);, the program stops, so there is no thread 'main' panicked at src/main.rs:26:13: or backtrace note.
Try it:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized +debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
The concept of closures will be covered in the next chapter, so it is fine if this does not make complete sense yet; a rough understanding is enough here.
12.4.5 The Full Code
Here is all the code written up to this article:
use std::env;
use std::fs;
use std::process;
struct Config {
query: String,
filename: String,
}
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);
});
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong while reading the file");
println!("With text:\n{}", contents);
}
impl Config {
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();
Ok(Config { query, filename})
}
}
Top comments (0)