clap icon indicating copy to clipboard operation
clap copied to clipboard

Support for using ArgGroup as Enum with derive

Open ModProg opened this issue 2 years ago • 19 comments

Maintainer's notes

  • Deriving Args on an enum would create a ArgGroup::new("T")::multiple(false)
  • More builder methods can be accessed via #[group(...)] (split out as #4574)
    • We need to ensure overriding the group name works
  • Variants without a type would be flags
  • Tuple variants would be arguments like in a struct
  • Flattening tuple variants would use Args::group_id, flattening struct variants would use the Variant.
    • Flattened struct variant fields should have their Arg::id set to Variant::field, see #4699.
  • Likely, we'll need to refactor clap_derive so we can share code between structs and enums
  • Note that nesting groups is problematic atm. See #3165
  • Create a Traefik cookbook example based on notes in #4699

~~Blocked on #3165 as that will lay some of the initial groundwork and #4211 to provide a fallback mechanism~~

Related

  • #4697

Please complete the following tasks

  • [X] I have searched the discussions
  • [X] I have searched the existing issues

Clap Version

3.0.0-beta.2

Describe your use case

I have multiple filters of witch only one can be set (--running, --exited, --restarting,...).

Describe the solution you'd like

I would like something with this usage:

#[derive(Clap)]
pub struct App{
    #[clap(flatten)]
    pub state_filter: StateFilter,
}

#[derive(Args)]
pub enum StateFilter {
    /// Only running servers are returned
    #[clap(long)]
    Running,
    /// Only exited servers are returned
    #[clap(long)]
    Exited,
    /// Only restarting servers are returned
    #[clap(long)]
    Restarting,
    #[clap(long)]
    Custom(String),
}

Resulting in:

OPTIONS:
    --exited
           Only exited servers are returned
    --restarting
           Only restarting servers are returned
    --running
           Only running servers are returned
    --cusxtom <CUSTOM>

Alternatives, if applicable

I could use an arggroup for this currently, but I would still need to check each boolean separately and could not just easily (and readable) match it.

Additional Context

No response

ModProg avatar Jul 25 '21 20:07 ModProg

When I was implementing the Args trait, I was thinking of this case. I was assuming we could do

#[derive(Clap)]
pub struct App{
    #[clap(flatten)]
    pub state_filter: Option<StateFilter>,
}

#[derive(Args)]
pub enum StateFilter {
    /// Only running servers are returned
    #[clap(long)]
    Running,
    /// Only exited servers are returned
    #[clap(long)]
    Exited,
    /// Only restarting servers are returned
    #[clap(long)]
    Restarting,
}

(with an attribute on the enum to override the group name)

Though I hadn't considered

  • Option in the caller, I like it!
  • Enum variants as flags, which I also like!

epage avatar Jul 26 '21 14:07 epage

This would be lovely for my use case (an archive utility, where the operation modes are exclusive but otherwise share the same options, similar to tar). I find clap's subcommand system to be a bit heavy-handed for what I need, but I'm lacking another way to concisely describe and match "exactly one of these options" like is described here.

Skirmisher avatar Oct 30 '21 21:10 Skirmisher

You can workaround this by manually defining the arg group and adding the arguments to it. While its using structopt instead of clap-derive, the principle and calls are pretty much the same in this code of mine.

epage avatar Oct 31 '21 00:10 epage

The problem with argument groups and the struct equivalent is that you can't ask clap what option was specified, all you can do is check each individual option of the group for presence in the arguments. We're back to to arguments not really being parsed into something useful.

With subcommands, clab directly talls you what subcommand was found in the arguments, as either an Enum or as the string ID of the subcommand. In both cases you can actually match on something instead of having an if-else chain or iteration simply to know which one it was

0x5c avatar Nov 02 '21 04:11 0x5c

I have a perfect use case for this right now: my app can accept a config file, or (perhaps if the config is very short) a string directly in the command line. For instance:

$ app --config 'foo=bar,baz=bat'
$ app --config-file ../longer-config.txt

The config string is not designed to be parsed by the app; it's passed directly through to the output. I have no desire to parse it.

Ideally, I'd write something like this:

#[derive(Parser)]
struct RootArgs {
    #[clap(flatten)]
    config: ConfigStringOrFile,
}

#[derive(Args)]
enum ConfigStringOrFile {
    #[clap(short = 'c', long = "config")]
    String(String),
    #[clap(short = 'C', long = "config-file")]
    File(PathBuf),
}

fn main() {
    let args = RootArgs::parse();

    let config = match args.config {
        ConfigStringOrFile::String(s) => s,
        ConfigStringOrFile::File(path) => { 
            // Read config from file...
        },
    };

    // ...
}

MTCoster avatar Nov 02 '21 16:11 MTCoster

The problem with argument groups and the struct equivalent is that you can't ask clap what option was specified, all you can do is check each individual option of the group for presence in the arguments. We're back to to arguments not really being parsed into something useful.

Yes, its only a workaround to help keep people from being blocked and is not ideal. We are trying to focus on polishing up clap v3 before before working on new features. Once v3 is out (and maybe a little after as we do high priority work we postponed), we can look into this or mentoring someone who can look into this,

epage avatar Nov 02 '21 16:11 epage

I'd like to offer another example that I would like to support with this feature. It's slightly more complex than the existing one, so worth calling out explicitly I think. I have a tls configuration that requires three pem files, representing the root certificate, the application cert, and the application's private key. By convention, they all live in the same directory, as root.pem, cert.pem, and key.pem. I would like users to be able to pass a directory or a path to each file, but not both, meaning I would like something like:

#[derive(clap::Args)]
enum Pems {
    Dir { 
        #[clap(long)]
        pem_dir: PathBuf,
    }
    Files {
        #[clap(long)]
        key: PathBuf,
        #[clap(long)]
        cert: PathBuf,
        #[clap(long)]
        root: PathBuf,
    }
}

or

#[derive(clap::Args)]
enum Pems {
    Dir(PemDir),
    Files(PemFiles),
}
#[derive(clap::Args)]
struct PemDir {
    #[clap(long)]
    pem_dir: PathBuf,
}
#[derive(clap::Args)]
struct PemFiles {
    #[clap(long)]
    key: PathBuf,
    #[clap(long)]
    cert: PathBuf,
    #[clap(long)]
    root: PathBuf,
}

jcdyer avatar Mar 03 '22 18:03 jcdyer

For what it's worth, I thought I understood the clap "language" of modeling out commands and flags and naturally reached for this and it took me a while to realize (and find this issue) that it wasn't possible.

I'm making do with the ArgGroup workaround @epage helpfully provided, so all is fine, just a shame we can't leverage what would have been a pretty elegant solution (for now?):

#[derive(Parser, Debug)]
#[clap(group = clap::ArgGroup::new("image-tag").multiple(false))]
pub struct ImageTag {
    #[clap(long, group = "image-tag")]
    existing: bool,

    #[clap(long, group = "image-tag")]
    new: bool,

    #[clap(long, group = "image-tag")]
    tag: Option<String>,
}

versus something like:

#[derive(Parser, Debug)]
pub enum ImageTag {
    Existing,
    New,
    Tag(String)
}

Just wanted to register my interest. Thanks for providing a workaround!

blaenk avatar Mar 22 '22 02:03 blaenk

Are there any plans to implement this yet, now that clap v3 has been out for a while? I did take a crack at it after I first commented here (about 9 months ago now...); I tried a couple different approaches, angling to adapt existing ArgGroup code to accept enum variants, but I kept running into flaws in my type model that made the whole thing fall apart. It didn't help that it was my first experience writing proc macro code, either, though I got impressively far considering. All that being said, if someone can come up with a sound design, I might be able to help with implementation.

Skirmisher avatar Jun 22 '22 03:06 Skirmisher

Are there any plans to implement this yet, now that clap v3 has been out for a while? I

This is not a priority of mine. My focus is on binary size, compile times, and extensibility. If someone else wants to work on a design proposal and then do an implementation, they are free to do so.

epage avatar Jun 22 '22 14:06 epage

I have encountered a situation where supporting this would be extremely useful. Specifically I am wanting to provide different authentication methods for a service. The following is a rough sketch of how I was imagining such a feature:

#[derive(Debug, Args)]
struct UserPassAuth {
    #[clap(short, long)]
    username: String,
    #[clap(short, long)]
    password: String,
}

#[derive(Debug, Args)]
struct TokenAuth {
    #[clap(long)]
    token: String,
}

#[derive(Debug, Default, MutuallyExclusiveArgs)]
enum AuthenticationMethod {
    #[default]
    Anonymous,
    UserPass(UserPassAuth),
    Token(TokenAuth),
}

This isn't necessarily a priority in that argGroups do exist, and they can be made mutually exclusive through that mechanism, but this does significantly clean up the interface that you have to deal with externally. Without enums you have to check what flags have been set, even if argGroups exist. With enums in theory it's as simple as:

match authentication {
   AuthenticationMethod::Anonymous => anonymous_login(),
   AuthenticationMethod::UserPass(login_info) => login(login_info),
   // ...
}

I'm not nearly confident enough to take a stab at implementing it, but if there's guidance on how this might be correctly implemented I would be interested in having direction.

StripedMonkey avatar Jul 17 '22 02:07 StripedMonkey

I'd like to get to this during 4.x series.

Some ground work I need to do in 4.0.0

  • Reserve the group attribute
  • Add an empty ArgGroup::new("TypeName") to each Args derive.
  • Add a fn group_id(&self) -> Option<&str> to AugmentArgs
    • Technically this can be done later.

epage avatar Sep 13 '22 00:09 epage

@epage I see this is tagged with "e-help wanted". Is there anything specifically I can help with? I'm pretty interested in this.

klnusbaum avatar Jan 07 '23 22:01 klnusbaum

@klnusbaum a good introduction to the code base that I'm assuming you'll want anyways is https://github.com/clap-rs/clap/issues/4574. If you want to take that, feel free to ask for more details in that issue.

Note that this one is also tagged E-Hard. We have a derive the struct derive for Args and we'd need to add enum support (which subcommand support can serve as an example of enum handling). A lot of code will be shared with struct support, a lot of code will be unique. It likely will take doing a prototype implementation that will be thrown away to learn enough to know how to share, if at all.

epage avatar Jan 08 '23 01:01 epage

@epage that all sounds total reasonable. I'll start with #4574 and see how far I can get from there. If it turns out I think I might having something, I will indeed start with a prototype.

klnusbaum avatar Jan 08 '23 03:01 klnusbaum

Correct me if I'm wrong, the feature that is being requested here would allow parsing mutually exclusive sets of flags into different variants of an enum, right? I really want to remove all the conflicts_with and required_by in my code.

KSXGitHub avatar May 18 '23 08:05 KSXGitHub

Yes, this would allow conflicts to be expressed in the type system

epage avatar May 18 '23 14:05 epage

Any updates on this? Is someone working on this?

stevenroose avatar Aug 08 '23 17:08 stevenroose

Not at this time

epage avatar Aug 08 '23 17:08 epage