Structopt: Support subcommand

Created on 7 Feb 2017  路  18Comments  路  Source: TeXitoi/structopt

The idea would be to create StructOpt struct for each subcommand, and then dispatching on an enum on the principal StructOpt.

enhancement

Most helpful comment

Added nested subcommands in https://github.com/williamyaoh/structopt/commit/d5f029a6d32a5a46e6a135f66167aa0770c6dfae
An example of nested subcommands + global flags:

#[derive(StructOpt)]
#[structopt(name = "foo-compiler")]
struct Args {
    #[structopt(short = "v", long = "verbose")]
    verbose: bool,
    #[structopt(subcommand)]
    cmd: Sub
}

#[derive(StructOpt)]
enum Sub {
    #[structopt(name = "compile")]
    Compile {
        #[structopt(short = "O", long = "opt")]
        optimization_level: u64,
        module: String,
        #[structopt(subcommand)]
        target: TargetType
    },
    #[structopt(name = "test")]
    Test {
        #[structopt(long = "iterations", default_value = "5")]
        iterations: u32,
        #[structopt(subcommand)]
        target: TargetType
    }
}

#[derive(StructOpt)]
enum TargetType {
    #[structopt(name = "debug")]
    Debug {},
    #[structopt(name = "release")]
    Release {},
    #[structopt(name = "doc")]
    Doc {}
}

Examples of allowed input:

  • foo-compiler --verbose compile -OOO some-module release
  • foo-compiler test --iterations 10 doc
  • foo-compiler compile another-module debug

I'll pretty it up and write some documentation, then open a PR for it. But for now, sleep...

All 18 comments

So, something like this?

#[derive(StructOpt, Debug)]
#[structopt(name = "git", about = "Because git is great!")]
enum GitCommand {
    #[structopt(name = "fetch")]
    Fetch {
        #[structopt(help = "Remote to fetch from", default_value = "None")]
        remote: Option<&str>, // Ignoring lifetime here
        #[structopt(help = "Branch to fetch from", default_value = "None")]
        branch: Option<&str> // Ignoring lifetime here
    },
    #[structopt(name = "commit")]
    Commit {
        #[structopt(short = "a", long = "all", help = "Adds all tracked files to the index before commit", default_value = "false")]
        all: bool,
        #[structopt(short = "p", long = "patch", help = "Use interactive patching interface for staging", default_value = "false")]
        patch: bool,
        #[structopt(short = "m", long = "message", help = "Add a commit message via CLI args instead of using $EDITOR", default_value = "None")]
        message: Option<&str> // Ignoring lifetime here
    }
}

fn main () {
    match Opt::from_args() {
        Fetch { remote, branch } => {
            //...
        },
        Commit { all, patch, message } => {
            //...
        }
    }
}

The idea was:

#[derive(StructOpt)]
#[structopt(name = "git", about = "Because git is great!")]
struct App {
    #[structopt(subcommand)]
    subcommand: SubCommand,
}
#[derive(StructOptSubCommand)]
enum SubCommand {
     #[structopt(name = "fetch")]
    Fetch(Fetch),
    #[structopt(name = "commit")]
    Commit(Commit),
}
#[derive(StructOpt)]
struct Fetch {
    #[structopt(help = "Remote to fetch from")]
    remote: Option<String>,
    #[structopt(help = "Branch to fetch from")]
    branch: Option<String>,
}
#[derive(StructOpt)]
struct Commit {
    #[structopt(short = "a", long = "all", help = "Adds all tracked files to the index before commit")]
    all: bool,
    #[structopt(short = "p", long = "patch", help = "Use interactive patching interface for staging")]
    patch: bool,
    #[structopt(short = "m", long = "message", help = "Add a commit message via CLI args instead of using $EDITOR")]
    message: Option<String>,
}

But I like your idea a lot. My solution is more flexible, but yours is much more compact for simpler use case.

Some comments on your proposition:

  • we can't have &str in the struct, but just using String do the trick

    • you don't want default_value for bool and Option, it will not do what you want (nothing for bool, you'll get Some("None") for Option

How do you get a None for Options?

By omitting an optional argument.

This is the biggest feature I'd like.

To conquer the world, this one feature you must have :D

so couple things: why do you need different deriving name for enums? Afaik you can't derive currently on an enum, so shouldn't that automatically make it a sub command?

Similarly, if the struct type is an enum, can you just infer that it's a sub command and not require annotation in the field ?

Also I'm hoping I can have regular struct opt annotations and then incrementally "drop in" a subcommand by making it a member of the field, which will only work as a subcommand if that keyword is passed on the cl, but the "top level" flags are still ok.

I ask because this is my favorite feature of struct opt; I can easily remove add new command line arguments with essentially no effort and then it's just there and ready for messing around with. In fact I frequently just add a switch temporarily when testing a feature just because it's so painless, and then remove it just as easily later. so I'm hoping subcommands will have similar ergonomics.

Also: keep up the good work !!! :heart:

@TeXitoi When you said that your solution was more flexible, were you referring to the possibility of having nested subcommands?

@m4b

Similarly, if the struct type is an enum, can you just infer that it's a sub command and not require annotation in the field ?

If you're talking about the #[structopt(subcommand)] annotation on enum variants, then this isn't possible to omit because of the way procedural macros currently work. For a variant like #[structopt(subcommand)] Fetch(Fetch) above, while we're deriving the implementation, the only information we have about the inner Fetch is its name, not whether it's an enum or a struct.

I don't understand, derive StructOpt should have full type information on the struct. Consequently if it encounters an enum element it shouldn't need an annotation, iiuc, since only enums in this case would be interpreted as a subcommand.

Or am I missing something ?

@m4b Basically, each #[derive(StructOpt)] (or other custom derive annotation) has to be processed independently of all other derive annotations. So each derive has no information about the other derives in the file/crate/whatever, and wouldn't be able to "see" the structure of the other type definitions, but only the type name itself. Like I said, it's a limitation on the way procedural macros are implemented in Rust right now.

More concretely, the source code for the struct directly after a custom derive annotation (and only that struct's code) will be fed to a function, which then has to use that code to spit some other code out, in this case an impl block. To be precise, said function has a syn::DeriveInput as input; if we drill down into a struct Field, and then into the Type of that field, what we'd get from the example I was talking about earlier would look something like Ty::Path(None, Path(vec!["Fetch"], ..)). The only information we have about the type is a string of the type identifier.

It seems like this should be directly analogous to other #[derive]-based deserialization methods, e.g. serde. Supporting enums fully should both enable more familiar types of command-line interfaces (e.g. subcommands) and more direct access for users to the ADTs required as input by a program.

While we're at it, it may be nice to have the default textual representation of enum variants use lowercase.

More flexible because

  • can have global flags
  • can reuse struct for different subcommands
  • can do subsubcommands (I admit I didn't thought about that initially)

Okay, I've got @ErichDonGubler's solution working at https://github.com/williamyaoh/structopt/commit/f321bcade898ef3a3f14562be345af7bbf142946. Next is extending that to support nested subcommands.

Added nested subcommands in https://github.com/williamyaoh/structopt/commit/d5f029a6d32a5a46e6a135f66167aa0770c6dfae
An example of nested subcommands + global flags:

#[derive(StructOpt)]
#[structopt(name = "foo-compiler")]
struct Args {
    #[structopt(short = "v", long = "verbose")]
    verbose: bool,
    #[structopt(subcommand)]
    cmd: Sub
}

#[derive(StructOpt)]
enum Sub {
    #[structopt(name = "compile")]
    Compile {
        #[structopt(short = "O", long = "opt")]
        optimization_level: u64,
        module: String,
        #[structopt(subcommand)]
        target: TargetType
    },
    #[structopt(name = "test")]
    Test {
        #[structopt(long = "iterations", default_value = "5")]
        iterations: u32,
        #[structopt(subcommand)]
        target: TargetType
    }
}

#[derive(StructOpt)]
enum TargetType {
    #[structopt(name = "debug")]
    Debug {},
    #[structopt(name = "release")]
    Release {},
    #[structopt(name = "doc")]
    Doc {}
}

Examples of allowed input:

  • foo-compiler --verbose compile -OOO some-module release
  • foo-compiler test --iterations 10 doc
  • foo-compiler compile another-module debug

I'll pretty it up and write some documentation, then open a PR for it. But for now, sleep...

@williamyaoh this is great! I've played with it on a project of mine and it works exactly as expected. There are one or two things that look like bugs, but don't seem fatal if you want to get things merged sooner rather than later:

  • #[structopt(name = "foo-bar")] for subcommand variants gets turned into foo - bar (with a space) making it awkward to use subcommands with hyphens

  • there doesn't seem to be a way to set the help text for subcommands? For example, #[structopt(name = "commit", help = "Record changes to the repository")] doesn't include the help message in --help output. I wouldn't call this a bug except that it looks bad that the top-level help subcommand gets an automatic description and the rest look out of place without them.

Thanks for getting an implementation working! :100:

Documentation added and PR opened. Let's move discussion of what still needs to be added to #17.

Closed by #17 available in 0.1.0

Is there some way to use a struct for subcommands instead of an enum? As far as I can tell, each new argument within a subcommand places a maintenance burden on any code which uses arguments from the subcommand by requiring them to alter their match statements to include the new argument. Is there some other way to use the enums that I'm not aware of that doesn't suffer from this drawback?

edit:
nevermind, I think I found a pattern that works by having a single argument for the enum subcommand which is another StructOpt struct.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

epage picture epage  路  7Comments

sphynx picture sphynx  路  9Comments

sharksforarms picture sharksforarms  路  6Comments

jackjen picture jackjen  路  7Comments

coder543 picture coder543  路  9Comments