Setting up git-like subcommands using structopt

Chris Biscardi
InstructorChris Biscardi
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 4 years ago

The StructOpt crate allows us to derive our human person functionality from our Struct definitions.

We will create a structop for our write subcommand that will take a short and long flag for our title argument


cargo add structopt

structopt allows us to derive argument parsing functionality from our structs. The name is a play on struct Opt {}. To do this we need to derive(StructOpt) on our struct. In this case we have a subcommand, so we'll set up one of the fields on the Opt struct to be a structopt(subcommand). This subcommand value will be an enum, indicating that only one subcommand can be used at a time.

structopt also gives us tools to specify one of three argument types:

  • positional
  • short
  • long

By default every argument is a positional argument, which means that in this case if we left it as-is, the CLI would function as

garden write my-title

We want to use a flag, so we'll use structopt to generate short and long flags for the title argument with structopt(short, long). That gives the use the option of -t and --title to specify a title.

structopt also contains some type processing, so all we need to do is specify that title is an optional string and the flags will become optional. That is: a user can specify or not specify a title.

struct Opt {
    #[structopt(subcommand)]
    cmd: Command,
}
enum Command {
    Write {
        #[structopt(short, long)]
        title: Option<String>,
    },
}

After setting up struct Opt, we can parse the args and use the dbg!() macro to print them to the console so we can see what they look like.

let opt = Opt::from_args();
dbg!(opt);

To do that we also need to derive(Debug) in addition to structopt on both the enum and the struct.

Given input like

garden write

We will see the following output.

[src/main.rs:21] opt = Opt {
    cmd: Write {
        title: None,
    },
}

We can choose to cargo build and then ./target/debug/garden write or we can use cargo run -- write. The -- ensures that our arguments get passed to the CLI we're building and not cargo run. This is especially relevant when passing something like --help.

Instructor: [0:00] The StructOpt crate allows us to derive our human person functionality from our Struct definitions. The name is a play on Struct Opt and allows us to derive the StructOpt trait, as well as specify various attributes, which will indicate how to handle various fields in our Struct.

[0:21] We'll add StructOpt to our project using cargo-edit like we did with color-eyre. Note that in our Cargo.toml, StructOpt showed up at version .3.21.

[0:32] Back in our main .rs, we can use structopt::StructOpt, and then we can write our structs. In this case, we have an option struct with a subcommand field called Command. This means this command enum will be what defines our different subcommands. We use an enum here and not a struct because while structs can have multiple fields at the same time, an enum can only have one option at one time.

[1:00] If we had another variant in our enum, say, Search, we could only use Write or Search. We couldn't use both at any given time as a user of the CLI. Our subcommand enum has a single Write variant right now with an argument called title as an optional string.

[1:22] By default, all fields are positional arguments when it comes to deriving StructOpt. That means if we left it as it is right now, the command would work like, garden Write my title. This isn't what we wrote in the read me in the beginning, so we'll choose to change it.

[1:39] In the StructOpt attribute macro on the field level, we can use short and long to both account for -t and --title in our CLI. Note that because we're deriving this, StructOpt will inspect the type of title and know that this is an optional flag. After we install color-eyre, we can let opt = Opt::from_args.

[2:06] From_args is a function placed on the Opt struct by deriving StructOpt on it. We'll also debug using the Debug macro to display any of the arguments that get passed in. This will let us visually debug as we get started and get used to using StructOpt.

[2:25] We can cargo run, and cargo will download and build StructOpt into our binary. Note that when I run cargo run with no arguments, we get help output. That help output also includes subcommands, help, which prints this message, and write, which we haven't documented yet.

[2:45] If we want to call the write subcommand for our binary, we can do, cargo run write. The application crashes, as expected, because we still have todo, but we also get the Debug output. The debug macro gives us a file name, a line number, the name of the argument, and the value it equals.

[3:08] In this case, we have an Opt struct with a command field that contains a write variant with a title of none, because we didn't pass one in. This Debug macro is also why we needed to derive Debug when we derived StructOpt. If we didn't derive Debug, we would get the Opt struct could not be formatted using the Debug formatter.

[3:33] There's a couple lines of help telling us that Debug trait is not implemented for Opt and a suggestion to even derive Debug or manually implement it. It also tells us why that's required. If we put the Debug trait back in the derive call and we call our binary directly, we can see what it'll look like when a user uses it for the first time.

[3:57] When we Path-T because we specified the short flag on the title field in our write variant, we can see that the title field is, some a.title. Some is an Option type, which is what we specified here.