Clap can parse a whole bunch of different values. and Rust has a type that represents file paths: PathBuf
.
Bring PathBuf
into scope.
use std::{fs, path::PathBuf};
The Rust PathBuf
type contains two very useful functions for us: exists
and join
.
Start out by changing the type of output_dir
to PathBuf
.
/// Where to put the file
#[clap(short, long, default_value = "content")]
output_dir: PathBuf,
And our filename
now makes use of join
instead of being the more error-prone format
macro.
let filename = args.output_dir.join(args.title);
Running the program at this point results in a compile error
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
error[E0277]: `PathBuf` doesn't implement `std::fmt::Display`
--> src/main.rs:40:43
|
40 | "failed to write file at `{filename}`\n\t{}",
| ^^^^^^^^^^ `PathBuf` cannot be formatted with the default formatter; call `.display()` on it
|
= help: the trait `std::fmt::Display` is not implemented for `PathBuf`
= note: call `.display()` or `.to_string_lossy()` to safely print paths, as they may contain non-Unicode data
= 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)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `scaffold` (bin "scaffold") due to previous error
This happens because the PathBuf
type in Rust’s standard library has to support a whole host of use cases that probably aren’t relevant to our brand new CLI tool.
Unicode and utf-8 weren’t always around, so some filepaths could be non-utf-8 and thus they can’t have Display
implementations.
We’ll cover two options to solve this.
PathBuf .display()
The first is using the .display()
function on PathBuf
. Move the filename
out of the format string and into the first argument so that we can call .display
on it.
format!(
"failed to write file at `{}`\n\t{}",
filename.display(),
error
),
This leads us to another use of moved value
compilation issue.
❯ cargo run
Compiling scaffold v0.1.0 (/rust-adventure/scaffold)
error[E0382]: use of moved value: `args.title`
--> src/main.rs:35:46
|
33 | let filename = args.output_dir.join(args.title);
| ---------- value moved here
34 |
35 | if let Err(error) = fs::write(&filename, args.title) {
| ^^^^^^^^^^ value used here after move
|
= note: move occurs because `args.title` has type `std::string::String`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0382`.
error: could not compile `scaffold` (bin "scaffold") due to previous error
The solution to this is the same as last time: clone or share references. I chose to use a shared reference to args.title
.
let filename = args.output_dir.join(&args.title);
We can also take this opportunity to add our file extension back in. Since we’re using PathBuf
, we can use the set_extension
` function to, well, set the file extension.
set_extension
accepts &mut self
as an argument, so we have to add mut
to filename to be able to use it.
let mut filename = args.output_dir.join(&args.title);
filename.set_extension("md");
This works just fine, but there’s another way to handle paths.
camino
The camino
crate restricts paths to be utf-8 valid, which is probably what you want. This additional restriction means we get to avoid calling .display
.
cargo add camino
Then output_dir
becomes the camino::Utf8PathBuf
type.
/// Where to put the file
#[clap(short, long, default_value = "content")]
output_dir: Utf8PathBuf,
don’t forget to bring the Utf8PathBuf
type into scope at the top of the file.
use camino::Utf8PathBuf;
and we can remove the .display
in our format!
macro, and move the variables back into the format string.
format!(
"failed to write file at `{filename}`\n\t{error}",
),
PathBuf.exists()
Finally, we can use the new functionality of the Utf8PathBuf
to check if the output directory exists and produce a more specific error message if it doesn’t.
Both the standard library PathBuf
and the camino::Utf8PathBuf
have the exists
function, which we can use to check if the directory exists or not.
fn main() {
let args = Args::parse();
dbg!(&args);
if !args.output_dir.exists() {
let mut cmd = Args::command();
cmd.error(
ErrorKind::ValueValidation,
format!(
"output directory `{}` doesn't exist",
args.output_dir
),
)
.exit();
}
...
}
I’ve chosen an ErrorKind
of ValueValidation
, but its not terribly important to get this right since we’re displaying the message directly to a user and not actually using the ErrorKind
value.
Now our first error checks to see if the output directory exists and produces a specialized message.