In the last lesson we encountered a function that could possibly return an error in the form of a Result when writing a new file. We used .unwrap() to panic the program when the error occurred, but Clap includes functionality to display errors as well.
To take advantage of this, we'll end up bringing in ErrorKind and CommandFactory from clap.
use clap::{error::ErrorKind, CommandFactory, Parser};
use std::fs;
and our main function will end up looking like this.
fn main() {
let args = Args::parse();
dbg!(&args);
let filename =
format!("{}/{}.md", args.output_dir, args.title);
if let Err(error) = fs::write(&filename, args.title) {
let mut cmd = Args::command();
cmd.error(
ErrorKind::Io,
format!(
"failed to write file at `{filename}`\n\t{}",
error
),
)
.exit();
}
}
if let
Since fs::write returns an io::Result<()>, we can use if-let syntax to take action if the Result is an Err variant. Remove the .unwrap() because we'll be matching on the return value instead, and move the fs::write into the following if-let syntax.
The syntax for if-let allows us to match on the structure of the Result returned from fs::write. In this case, we've tested to see if the Result is an Err variant, and also destructured the error value contained in the Err so that we can access it in our code.
if let Err(error) = fs::write(filename, args.title) {
dbg!("there was an error", error);
}
dbg! then allows us to print out a message and the error, if there was an error.
Remove the content directory if you created it in the last lesson, and run the program to see the error.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
Finished dev [unoptimized + debuginfo] target(s) in 0.49s
Running `target/debug/scaffold`
[src/main.rs:32] &args = Args {
layout: "post",
tags: [],
title: "A Post",
status: "draft",
output_dir: "content",
}
[src/main.rs:37] "there was an error" = "there was an error"
[src/main.rs:37] error = Os {
code: 2,
kind: NotFound,
message: "No such file or directory",
}
The error in this case is an error from the Os, or operating system, that has a code of 2, and is functionally a NotFound kind of error, with a user-displayable message "No such file or directory".
Clap's CommandFactory
Clap's Parser trait is a convenience for a combination of some other traits, one of which is a CommandFactory.
CommandFactory includes a function called command that is automatically derived for us as part of the Parser derive on our Args struct. The command function returns a Command struct, which has the function that we care about on it: error.
So inside of our if-let, we can access the Command by using the command function on our Args struct.
We need the variable cmd to be mut specifically because the error function accepts an argument of &mut self, which is an exclusive reference.
if let Err(error) = fs::write(filename, args.title) {
let mut cmd = Args::command();
cmd.error(
ErrorKind::Io,
format!(
"failed to write file at `{filename}`\n\t{}",
error
),
)
.exit();
}
The error function is associated with Command, so if we access self at that point, self is the Command we are using!
The error function returns a clap::error::Error struct, which has an associated function called exit that we can then use to exit the app immediately.
The error function itself accepts a clap::error::ErrorKind, and something that impl Display, which basically means anything that implements the Display trait can be passed in as the second argument.
Strings implement the Display trait, so we can use the format! macro to create a new String to show to the user.
The format! macro allows us to pass in variable names between the curly braces, like {filename}, as well as passing arguments after, which will be placed into the next applicable formatter.
In this case I added a newline and a tab before displaying the actual error to the user.
format!(
"failed to write file at `{filename}`\n\t{}",
error
),
error[E0382]: borrow of moved value
If we run the application so far, we run into a compilation error that tells us the program is attempting to borrow a moved value. In other words, the program is trying to use a value that got moved somewhere else before we tried to use it.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
error[E0382]: borrow of moved value: `filename`
--> src/main.rs:41:44
|
33 | let filename =
| -------- move occurs because `filename` has type `std::string::String`, which does not implement the `Copy` trait
...
36 | if let Err(error) = fs::write(filename, args.title) {
| -------- value moved here
...
41 | "failed to write file at `{filename}`\n\t{}",
| ^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::__export::format_args` which comes from the expansion of the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
36 | if let Err(error) = fs::write(filename.clone(), args.title) {
|
The help message suggests cloning the value, which will totally work! Cloning is making a duplicate of the value, so when we move the value filename into the write function, cloning that value passes the newly created duplicate into the write function instead.
This means we still have the original value around to use when we format! later.
if let Err(error) =
fs::write(filename.clone(), args.title)
{
let mut cmd = Args::command();
cmd.error(
ErrorKind::Io,
format!(
"failed to write file at `{filename}`\n\t{}",
error
),
)
.exit();
}
In our case, cloning works perfectly fine and we get a result with our new error.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/scaffold`
[src/main.rs:32] &args = Args {
layout: "post",
tags: [],
title: "A Post",
status: "draft",
output_dir: "content",
}
error: failed to write file at `content/A Post.md`
No such file or directory (os error 2)
Usage: scaffold [OPTIONS]
For more information, try '--help'.
But the write function also doesn't need to own the value we pass in. It only needs to look at the filename to write the file out, it doesn't need to modify the filename in any way.
So we also could have passed in a shared reference to the filename instead.
fn main() {
let args = Args::parse();
dbg!(&args);
let filename =
format!("{}/{}.md", args.output_dir, args.title);
if let Err(error) = fs::write(&filename, args.title) {
let mut cmd = Args::command();
cmd.error(
ErrorKind::Io,
format!(
"failed to write file at `{filename}`\n\t{}",
error
),
)
.exit();
}
}