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"