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.