Adding support for global flags that apply for all subcommands with structopt

Chris Biscardi
InstructorChris Biscardi
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 4 years ago

We want to build out functionality to let the user write into a file in their garden. To do that, we have to know the location of their garden.

We'll start by implementing a function to get the default garden path.

fn get_default_garden_dir() -> Result<PathBuf> {
    let user_dirs = UserDirs::new().ok_or_else(|| eyre!("Could not find home directory"))?;
    Ok(user_dirs.home_dir().join(".garden"))
}

This uses the user's system directories via the directories crate to find their home dir. We then add .garden to the home dir for the default garden.

Ideally, we would want the user to be able to specify their garden path. Using structopt, we can let them specify it as a short, long, or env var.

In our README, we'll specify how this should work.


```shell
GARDEN_PATH=~/github/my-digital-garden garden write
garden -p ~/github/my-digital-garden write
garden --garden_path ~/github/my-digital-garden write
```

And in our Opt struct, we can tell structopt how to handle it. Putting this in our Opt struct instead of our Command enum make it a global flag that can be specified for all subcommands.

struct Opt {
    #[structopt(parse(from_os_str), short = "p", long, env)]
    garden_path: Option<PathBuf>,

    #[structopt(subcommand)]
    cmd: Command,
}

structopt is only handling the user input, so in main() we still need to fall back to the default if the garden_path is not specified.

let garden_path = match opt.garden_path {
    Some(pathbuf) => Ok(pathbuf),
    None => get_default_garden_dir().wrap_err("`garden_path` was not supplied"),
}?;

if we force an Err in get_default_garden_dir We can see how the wrapped errors end up stacking.

Error:
   0: `garden_path` was not supplied
   1: Could not find home directory

We'll also change the signature of write to accept the pathbuf.

Command::Write { title } => write(garden_path, title),

Note that we need to import PathBuf. We can also dbg! the garden path and title to see their values.

use std::path::PathBuf;

pub fn write(garden_path: PathBuf, title: Option<String>) -> Result<()> {
    dbg!(garden_path, title);
    todo!()
}

Chris Biscardi: [0:01] Now we have a write function, which is what we're going to use to write files out to disk, but we don't know where to put them yet. We'll start by going back to main.rs and writing a little utility function. The function name is going to be get_all_garden_dir, and it's going to return us a result with a PathBuf. This means if everything succeeds, we get a path buffer, and if it fails, we get an error.

[0:30] We're going to take advantage of the directories crate to get the user directories. If we can find the home directory, we'll join it with .garden to give us the location of the user's default garden, as it will be home_dir/.garden.

[0:47] Since I'm using rust-analyzer, I can use Command-dot on a Mac to bring up this little menu and import std::path::PathBuf. If I scroll up, you can see that I used std::path::PathBuf format.

[1:03] We can't do the same for UserDirs, because the crate isn't installed yet. Let's stop our watch process and run cargo add directory. Then we can start our watch process again. Note that cargo-check was telling us that we might want to use directories::UserDirs, because we are using in our function.

[1:26] We have one more identifier to bring into scope, and that's Err. We can use the rust-analyzer autocomplete to import, and call it Err(err), but that's not actually what we want. We have to go up and text it what we want, let's call it Err(Err(err)), which is a macro. It's a macro for creating new errors. UserDirs::new() returns an option, so we can turn that into a result with ok_or_else.

[1:58] Ok_or_else takes closure, that takes no arguments and allows us to give an error value. This is important because we're adding context to why the UserDirs failed. Not that because we have a result after using ok_or_else. We can use the question mark to early return if there's an error.

[2:23] If we have a valid UserDirs value, we can get the home_dir and then join it with the garden. Ideally, we don't just supply the location of a garden. The user will probably want to specify that themselves. Using StructOpt, we can let them specify as a short or long's leg or an environment variable.

[2:44] If we go back to our readme, we can specify how this should work. Under commands, we'll add a section called setting the garden path.

[2:55] This will allow us to specify the environment variable as a location for garden right, specify a short flag -p, or a long flag, --garden path. Back in our Opt struct, we can tell StructOpt exactly what we want to see. Garden path, it's going to be an optional path buff, because the user can supply it or not. The short version is going to be p, the long version is going to be automatically derived.

[3:28] We're also going to be able to specify it as an environment variable. Note also that we've used a custom parse function, because path buffers are OS strings. OS strings can include characters that aren't valid UTF eight. Back in Main, we can specify the garden path as the result of a match on options garden path, options garden path being an optional type.

[3:52] If we have a path buffer, we're just going to return that path buffer, and that will become our garden path. This means the user specified one. If we match and there's none, then we're going to get the default garden dir. This is a result. We'll wrap this in an additional error context.

[4:11] Wrap error takes the error that happened, and places that as the source of our new error, adding additional context to what went wrong. To use wrap error on a result type, we need to bring in the wrap error trait from air. In Rust, functions that are added via traits aren't accessible unless the trade is in scope.

[4:37] With our tests passing, we'll choose to force an error and get default guard under to see what this looks like. Here we can see what happens if an error occurs and get the full garden dir. We can see garden path was not supplied, which is the context we wrapped our error in, then we can see the original error.

[4:57] While we forced an error here, the error that would normally occur is this could not find home directory, which is the message that would appear in error number one. The user will get the information that the garden path was not supplied, and we also couldn't find the home directory. If both of those occur, then we don't have a place to put the garden and the CLI tool cannot continue.

[5:22] We also have to change the signature of our write function to accept the garden path. We'll add the argument here, which rust-analyzer is correctly telling us doesn't accept two arguments right now, and we'll change write to accept PathBuf as the first argument.

[5:42] Again, we can use the rust-analyzer Command-dot to do the import for us, or we could write it out ourselves. I'm also going to debug the garden path and the title, so we can remove this placeholder.

[5:55] With everything type-checking and our tests passing, we'll run write, and now we can see the Opt struct and which subcommand we used logged out as well as inside of src/write, the garden path, which for me is Users/chris.garden, and the title that was given.

[6:15] We now have a user configurable garden path. If I wanted the garden path to be somewhere else, I could just say so. Note that the garden path will come in as somewhere else.