Often there are specific places on an operating system where different kinds of files are stored.
Our CLI doesn’t really have a pre-defined location, so we’ll store the default garden directory in the user’s home directory.
The directories crate can help us discover the right place on each operating system.
❯ cargo add directories
Updating crates.io index
Adding directories v5.0.1 to dependencies.
Updating crates.io index
In main.rs create a new function that returns an Option<PathBuf>. Its possible that we won’t be able to get the user’s home directory, so we need a concept of failure for this function. The UserDirs::new() function returns an Option<UserDirs> so we’ll continue using that as our return value.
If new() returns a Some value, we can map into that value and continue to use it.
UserDirs has a function to get the home directory named home_dir. that returns a Path, which has a join function we can use to place a garden directory in the user’s home directory.
/// Get the user's garden directory, which by default
/// is placed in their home directory
fn get_default_garden_dir() -> Option<PathBuf> {
UserDirs::new()
.map(|dirs| dirs.home_dir().join("garden"))
}
We also need to bring UserDirs into scope:
use directories::UserDirs;
While there’s some level of validation we can do “in-clap”, the easiest way to do some validation on a value is to just use the value in main after we’ve parsed it.
Validating the filepath
We’ve specified our garden_path flag as being optional, so we can take advantage of the functions associated with the Option type.
Option::or_else allows us to return the Some value if its a Some, and otherwise will call a function to create the Option. We just wrote a function that works here: get_default_garden_dir. Although we also could’ve used a closure.
We can then use let-else syntax to destructure the inner value out of the Option if the value is Some, or return an error in the else block.
The else block is interesting because it is typed as returning the ! type, which is the never type. This is a compiler-enforced constraint that the else block will not return a value as part of this expression.
The break, continue and return expressions are all typed as ! as well, so we could use return inside of the else block (for example) to return an error to the main function.
fn main() {
let args = Args::parse();
dbg!(&args);
let Some(garden_path) =
args.garden_path.or_else(get_default_garden_dir)
else {
let mut cmd = Args::command();
cmd.error(
ErrorKind::ValueValidation,
format!(
"garden directory not provided and home directory unavailable for default garden directory"
),
)
.exit()
};
if !garden_path.exists() {
let mut cmd = Args::command();
cmd.error(
ErrorKind::ValueValidation,
format!(
"garden directory `{}` doesn't exist, or is inaccessible",
garden_path.display()
),
)
.exit()
};
dbg!(garden_path);
}
In our case, we’ll construct a new clap error using Args::command() and .error, then exit() that error, which also evaluates to ! because it exits the program.
We construct an error in the same way in the next if expression, where we’re using the PathBuf::exists function to check to make sure the directory the user passed in exists.
If all goes well we have a PathBuf we can use for the garden directory in garden_path.
Hitting one of the errors tells the user something went wrong.
❯ cargo run -- write -t "My New Post"
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/garden write -t 'My New Post'`
[src/main.rs:42] &args = Args {
garden_path: None,
cmd: Write {
title: Some(
"My New Post",
),
},
}
error: garden directory `/Users/chris/garden` doesn't exist, or is inaccessible
Usage: garden [OPTIONS] <COMMAND>
For more information, try '--help'.
and if we pass through the checks because the directory exists, then we’ll see the path printed out. In this case I’m on a mac, so I’ve used mkdir to create the garden directory in my home directory.
❯ mkdir ~/garden
❯ cargo run -- write -t "My New Post"
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/garden write -t 'My New Post'`
[src/main.rs:42] &args = Args {
garden_path: None,
cmd: Write {
title: Some(
"My New Post",
),
},
}
[src/main.rs:68] garden_path = "/Users/chris/garden"