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();
}
}