Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose related and mutually exclusive arguments via struct / enum? #3115

Closed
epage opened this issue Dec 9, 2021 · 10 comments
Closed

Expose related and mutually exclusive arguments via struct / enum? #3115

epage opened this issue Dec 9, 2021 · 10 comments
Labels
A-derive Area: #[derive]` macro API

Comments

@epage
Copy link
Member

epage commented Dec 9, 2021

Issue by epage
Thursday May 03, 2018 at 02:38 GMT
Originally opened as TeXitoi/structopt#104


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,
}
@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by TeXitoi
Thursday May 03, 2018 at 10:00 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by epage
Thursday May 03, 2018 at 13:15 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by TeXitoi
Thursday May 03, 2018 at 13:46 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by epage
Thursday May 03, 2018 at 14:09 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by TeXitoi
Thursday May 03, 2018 at 15:09 GMT


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

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by porglezomp
Saturday May 05, 2018 at 05:51 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by sunshowers
Wednesday Mar 10, 2021 at 00:31 GMT


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.

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by nathan-at-least
Friday Aug 27, 2021 at 16:04 GMT


I just skimmed this because I want this feature. IIUC in TeXitoi/structopt#104 (comment) it is possible in clap, correct? If so, then only the structopt API needs to be defined. I'd be happy with the suggestion in TeXitoi/structopt#104 (comment) .

For my current case, I simply want exclusive options: either --verbose or --quiet or neither, but not both. Is this already expressable in structopt?

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Comment by TeXitoi
Friday Aug 27, 2021 at 16:18 GMT


@nathan-at-least https://github.com/TeXitoi/structopt/blob/master/examples/group.rs is almost what you want.

And what you want is:

use structopt::StructOpt;

#[derive(Debug, StructOpt)]
struct Opt {
    #[structopt(short, long, group = "verbosity")]
    verbose: bool,
    #[structopt(short, long, group = "verbosity")]
    quiet: bool,
    #[structopt(short, long)]
    name: Option<String>,
}

fn main() {
    let opt = Opt::from_args();
    println!("{:?}", opt);
}

@epage
Copy link
Member Author

epage commented Dec 9, 2021

Going to close this in favor of #2621

@epage epage closed this as completed Dec 9, 2021
@epage epage added the A-derive Area: #[derive]` macro API label Dec 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-derive Area: #[derive]` macro API
Projects
None yet
Development

No branches or pull requests

1 participant