Structopt: Expose related and mutually exclusive arguments via struct / enum?

Created on 3 May 2018  路  7Comments  路  Source: TeXitoi/structopt

I'm working on a CLI that currently looks like

#[derive(StructOpt, Debug)]
#[structopt(name = "staging")]
pub struct Arguments {
    #[structopt(short = "i", long = "input", name = "STAGE", parse(from_os_str))]
    pub input_stage: path::PathBuf,
    #[structopt(short = "d", long = "data", name = "DATA_DIR", parse(from_os_str))]
    pub data_dir: Vec<path::PathBuf>,
    #[structopt(flatten)]
    pub output: Output,
    #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
    pub verbosity: u8,
}

#[derive(StructOpt, Debug)]
pub struct Output {
    #[structopt(short = "o", long = "output", name = "OUT", parse(from_os_str))]
    pub dir: path::PathBuf,
    #[structopt(long = "format",
                raw(possible_values = "&Format::variants()", case_insensitive = "true"),
                raw(default_value = "DEFAULT_FORMAT"))]
    pub format: Format,
    #[structopt(short = "n", long = "dry-run")]
    pub dry_run: bool,
}

I'm looking at adding some options that are mutually exclusive with Arguments::output.

This gave me the ideas:

  • Allow flattened structs to define groups
  • Allow Option<Output> on an entire flattened struct to say the arguments require each other
  • Allow an enum to define mutually exclusive flags. For example:
#[derive(StructOpt, Debug)]
#[structopt(name = "staging")]
pub struct Arguments {
    #[structopt(short = "i", long = "input", name = "STAGE", parse(from_os_str))]
    pub input_stage: path::PathBuf,
    #[structopt(short = "d", long = "data", name = "DATA_DIR", parse(from_os_str))]
    pub data_dir: Vec<path::PathBuf>,
    #[structopt(flatten)]
    pub flags: Flags,
    #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
    pub verbosity: u8,
}

// `Output` as above

#[derive(StructOpt, Debug)]
pub enum Flags
{
    Output(Output),
    Completions(Completions),
    DumpConfig(DumpConfig),
    DumpData(DumpData),
}

#[derive(StructOpt, Debug)]
pub struct Completions {
    #[structopt(long = "completion", name = "OUT", parse(from_os_str))]
    pub completion: path::PathBuf,
}

#[derive(StructOpt, Debug)]
pub struct DumpConfig{
    #[structopt(long = "dump-config")]
    pub dump_config: bool,
}

#[derive(StructOpt, Debug)]
pub struct DumpData{
    #[structopt(long = "dump-data")]
    pub dump_data: bool,
}
enhancement need-design

Most helpful comment

I wanted something very similar to these proposals, and was actually slightly surprised when nothing like them was available. I'd imagine that you could have something like:

#[derive(StructOpt)]
#[structopt(flag_group)]
enum Mode {
    #[structopt(short = "s", long = "stack")]
    Stack,
    #[structopt(short = "q", long = "queue")]
    Queue,
}

#[derive(StructOpt)]
#[structopt(name = "letter")]
struct Opt {
    #[structopt(from_flag_group)]
    mode: Mode,
}

and then you can either use letter --stack or letter --queue.
This would generate a required group, and unpack the options into the enum.

All 7 comments

First thought on the proposition, that is interesting, but not yet mature enough:

  • derive(StructOpt) on an enum will do subcommand, thus it need something else.
  • flatten on an enum set subcommand for now, thus this feature might need a special keyword.
  • having DumpData just for a bool is quite sad, it would be better to have Completions, DumpConfig and DumpData` just be empty enum variant.
  • Completions would be better with just the PathBuf directly inside.

Maybe something like that:

#[derive(StructOpt, Debug)]
#[structopt(name = "staging")]
pub struct Arguments {
    #[structopt(short = "i", long = "input", name = "STAGE", parse(from_os_str))]
    pub input_stage: path::PathBuf,
    #[structopt(short = "d", long = "data", name = "DATA_DIR", parse(from_os_str))]
    pub data_dir: Vec<path::PathBuf>,
    #[structopt(group)]
    pub flags: Flags,
    #[structopt(short = "v", long = "verbose", parse(from_occurrences))]
    pub verbosity: u8,
}

#[derive(StructOptGroup, Debug)]
pub enum Flags
{
    // no annotation? an annotation?
    Output(Output), // <- How to handle that with clap?
    #[structopt(long = "completion", name = "OUT", parse(from_os_str))]
    Completions(PathBuf),
    #[structopt(long = "dump-config")]
    DumpConfig,
    #[structopt(long = "dump-data")]
    DumpData,
}

#[derive(StructOpt, Debug)]
pub struct Output {
    #[structopt(short = "o", long = "output", name = "OUT", parse(from_os_str))]
    pub dir: path::PathBuf,
    #[structopt(long = "format",
                raw(possible_values = "&Format::variants()", case_insensitive = "true"),
                raw(default_value = "DEFAULT_FORMAT"))]
    pub format: Format,
    #[structopt(short = "n", long = "dry-run")]
    pub dry_run: bool,
}

For this particular need, subcommands seem more appropriate, and is working today. But that may be interesting for other cases.

having DumpData just for a bool is quite sad, it would be better to have Completions, DumpConfig and DumpData` just be empty enum variant.

Yeah, it was more for illustrative purposes of the wider idea. As you say, even better if I can directly annotate an enum variant to say that its just a flag.

Output would be better with just the String directly inside.

Except that isn't what I was trying to point out. This is supposed to take in the Output struct. In clap, I'd say

  • the defaulted parts of the Output struct require --output.
  • The members of the Output struct are a group and that completions and dump conflict with the group.

For this particular need, subcommands seem more appropriate, and is working today. But that may be interesting for other cases.

I'd considered that but its pretty common for CLIs to not go the subommand route for non-routine alternative behaviors of the application

  • --version
  • --help
  • --completions
  • Creation of configuration files to jump start the user

At that point, it felt awkward to create subcommands just for my debugging, so I went ahead and made my dump state flags do similar.

Output would be better with just the String directly inside.

Sorry, I mean Completions

Except that isn't what I was trying to point out. This is supposed to take in the Output struct. In clap, I'd say

  • the defaulted parts of the Output struct require --output.

    • The members of the Output struct are a group and that completions and dump conflict with the group.

Is it possible to express such constraints in pure clap? because if that's not possible, we can't do it in structopt.

I'm also afraid that this machinery will be too complicated to be understood by the users. Finding a clear, flexible and usable interface is a challenge here.

Is it possible to express such constraints in pure clap? because if that's not possible, we can't do it in structopt.

Thee members of the Output struct are a group and that completions and dump conflict with the group.

    .group(ArgGroup::with_name("output_struct")
        .multiple(true)
        .args(&["dir", "format", "dry_run"])
        .conflicts_with_all(&["completions", "dump"]))

the defaulted parts of the Output struct require --output.

I'll admit, this one was more aspirational. One option is to iterate on the fields and, if there are required fields, to mark the optional fields as depending on the required fields.

I'm also afraid that this machinery will be too complicated to be understood by the users. Finding a clear, flexible and usable interface is a challenge here.

Are you referring to the developer or to the user?

For the user, I think its understandable that some arguments only work in some settings. I normally visually group these in the help (with python's argparse) but haven't played too much with doing that with clap yet.

For the developer, I'd say they are. These are the things I'm intuitively trying to do but can't. Instead I'm having to drop down into raw calls which, as I mentioned on another issue, have been challenging enough to get right, that I've just given up.

Yeah, I mean the user of StructOpt, the developer of the cli.

I wanted something very similar to these proposals, and was actually slightly surprised when nothing like them was available. I'd imagine that you could have something like:

#[derive(StructOpt)]
#[structopt(flag_group)]
enum Mode {
    #[structopt(short = "s", long = "stack")]
    Stack,
    #[structopt(short = "q", long = "queue")]
    Queue,
}

#[derive(StructOpt)]
#[structopt(name = "letter")]
struct Opt {
    #[structopt(from_flag_group)]
    mode: Mode,
}

and then you can either use letter --stack or letter --queue.
This would generate a required group, and unpack the options into the enum.

One random thought I had is that I've noticed some remarkable similarities between structopt's and serde's data models. It seems like subcommands are equivalent to serde's externally tagged enums, while mutually exclusive options, if modeled through enums, are similar to untagged enums. It may be worth aligning with serde's design in this respect.

One thing it suggests is the possibility for internally tagged enums, which may be reflected in the CLI as e.g. path/to/binary --command foo --arg1 x --arg2 y.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rask picture rask  路  8Comments

swfsql picture swfsql  路  3Comments

sharksforarms picture sharksforarms  路  6Comments

tathanhdinh picture tathanhdinh  路  6Comments

yoshuawuyts picture yoshuawuyts  路  7Comments