Picocli: Add support for mutual exclusive options

Created on 5 Oct 2017  ·  16Comments  ·  Source: remkop/picocli

Something like this:

@Command(autoHelp = true, sortOptions = false, groups = {
@OptionGroup(name = "dest", required = true, exclusive = true,
           heading = "Destination: The data can be sent either to a file or to a socket"),
@OptionGroup(name = "other",
           heading = "Other:%n  Some other options")})
class ExclusiveOptionsExample {

  @Option(names = "-f", description = "Target file", group = "dest")
  File file;

  @Option(names = "-s", description = "Target socket", group = "dest")
  InetAddress adr;

  @Option(names = "-d", description = "Data file", group = "other")
  File data;
}

Usage help:

Usage: <main class> [-hV] [-d=<data>] (-f=<file> | -s=<adr>)
Destination: The data can be sent either to a file or to a socket
  -f= <file>                  Target file
  -s= <adr>                   Target socket
Other:
  Some other options
  -d= <data>                  Data file
  -h, --help                  Show this help message and exit.
  -V, --version               Print version information and exit.
Commands:
  help  Displays help information about the specified command
API enhancement

Most helpful comment

A first cut of support for ArgGroups has landed in master. This adds support for:

  • mutually exclusive options and positional parameters (exclusive = true)
  • co-occurring options and positional parameters (exclusive = false: require all elements in the group, or none) as requested in #295
  • composite groups
  • groups can be required or optional

Example:

 @Command(argGroups = {
         @ArgGroup(name = "EXCL",      exclusive = true,  required = true),
         @ArgGroup(name = "ALL",       exclusive = false, required = true),
         @ArgGroup(name = "COMPOSITE", exclusive = false, required = false,
                   subgroups = {"ALL", "EXCL"}),
 })
 class App {
     @Option(names = "-x", groups = "EXCL") boolean x;
     @Option(names = "-y", groups = "EXCL") boolean y;
     @Option(names = "-a", groups = "ALL") boolean a;
     @Option(names = "-b", groups = "ALL") boolean b;
 }

This defines a composite group with the following synopsis:

 [(-x | -y) (-a -b)]

The composite group consists of the required exclusive group (-x | -y) and the required co-occurring group (-a -b).

Valid user input for this group would be:

  • no options at all (because the composite group itself is non-required)
  • -x -a -b (in any order)
  • -y -a -b (in any order)

Any other combination of -x, -y, -a and -b would give a validation error.


Still TODO:

  • [x] show group synopsis in usage help message
  • [x] show group headings in options list (as requested in #450)
  • [ ] resource bundle support for group headings in options list (related to #450)
  • [ ] update annotation processor in picocli-codegen to build a model from the new annotations at compile time
  • [ ] documentation in user manual and release notes

That said, the majority of the functionality is there.
I would appreciate it if people would try this to see if it meets their requirements and to find any places where the API, error messages or anything else should be improved.

All 16 comments

Also: Sometimes its common to have a single option as an alternative with a group of options. Like in the below -u, -p, -c are only required if -P (profile) is not present.

list-nodes -c <controlHost> -u <user> -p <pass>  OR list-nodes -P <profileName>

Thinking about this further, OptionGroups and individual Options could have a requires attribute (type String[]) that allows specifying a one-way dependency. The values may be the names of single options or option groups.

If an option “-x” requires an option group, then when “-x” is specified, either one member option becomes required (if the group is mutually exclusive) or all members of the group become required.

An option may belong to multiple groups.

Unsure if groups should have a hidden attribute to mean don’t impact the layout of the option list. Perhaps the other way around is better: only show groups in the option list when the group has a heading. Groups need an order attribute to allow applications to control where the group appears in the options list.

Mutually exclusive option groups should be rendered in the synopsis as [-a | -b | -c], or as (-a | -b | -c) when the group is required.

Non-exclusive option groups should be rendered in the synopsis as (-a -b -c) when the group is required, and as [(-a -b -c)] when not required.

When an option “-x” requires another option or option group, the synopsis should show multiple variations: one where the “-x” option is present (and the referenced options show as required) and one where the “-x” option is absent (and the referenced options show as optional).

––––

Update: the idea of having a requires attribute for specifying a one-way dependency would be complex to implement, difficult to understand and difficult to express in synopsis syntax. Better to define a group as either exclusive (require max one) or co-occurring (require-all-if-any-exists).

Would this support something like [( --json-def | --std-def | ( [--prop-a] [--prop-b] [--prop-c] ...))]? Ie. pass configuration as;

  • option for each property: --prop-a, --prop-b, --prop-c,
  • json file with definition: --json-def,
  • name of standard definition: --std-def.

And, it would be good to support multiple groups.

Would this only concern required options or could we imagine indicating exclusive option like -a can't be used if -b is defined ?

Individual options and option groups will have both a requires and an excludes attribute where the name of other options (or other groups) to require or exclude can be specified.

Use cases like “if -a is specified, -b is excluded” will certainly be supported, as well as “if -a is specified, -b is required”.

A first cut of support for ArgGroups has landed in master. This adds support for:

  • mutually exclusive options and positional parameters (exclusive = true)
  • co-occurring options and positional parameters (exclusive = false: require all elements in the group, or none) as requested in #295
  • composite groups
  • groups can be required or optional

Example:

 @Command(argGroups = {
         @ArgGroup(name = "EXCL",      exclusive = true,  required = true),
         @ArgGroup(name = "ALL",       exclusive = false, required = true),
         @ArgGroup(name = "COMPOSITE", exclusive = false, required = false,
                   subgroups = {"ALL", "EXCL"}),
 })
 class App {
     @Option(names = "-x", groups = "EXCL") boolean x;
     @Option(names = "-y", groups = "EXCL") boolean y;
     @Option(names = "-a", groups = "ALL") boolean a;
     @Option(names = "-b", groups = "ALL") boolean b;
 }

This defines a composite group with the following synopsis:

 [(-x | -y) (-a -b)]

The composite group consists of the required exclusive group (-x | -y) and the required co-occurring group (-a -b).

Valid user input for this group would be:

  • no options at all (because the composite group itself is non-required)
  • -x -a -b (in any order)
  • -y -a -b (in any order)

Any other combination of -x, -y, -a and -b would give a validation error.


Still TODO:

  • [x] show group synopsis in usage help message
  • [x] show group headings in options list (as requested in #450)
  • [ ] resource bundle support for group headings in options list (related to #450)
  • [ ] update annotation processor in picocli-codegen to build a model from the new annotations at compile time
  • [ ] documentation in user manual and release notes

That said, the majority of the functionality is there.
I would appreciate it if people would try this to see if it meets their requirements and to find any places where the API, error messages or anything else should be improved.

Back to the drawing board

The above design (currently in master) works well for many cases, but it has two major drawbacks:

  • it does not play nice with positional parameters
  • it does not support repeating groups

Positional Parameters

Problematic Example:

@Command(argGroups = @ArgGroup(name = "x", exclusive = false))
class App {
    @Option(names = "-a",    groups = "x") int a;
    @Parameters(index = "0", groups = "x") File file;
    @Parameters(index = "1") String string;
}

This gives a command with the following synopsis:

<cmd> [-a=<a> <file>] <string>

Based on the synopsis, we expect that both of the following invocations are valid:

# this works fine
<cmd> -a=123 /my/file somestring

# this fails: "Missing parameter <string>"
<cmd> somestring

The seconds invocation fails, because the "somestring" argument is assigned to the first positional parameter, the positional parameter with index 0, which is the file field...

The current parser implementation is not ArgGroup-aware; ArgGroups are validated _after_ the parser completes. This is clearly not sufficient.

Repeating Groups

This relates to #358. A common use case is to allow repeating groups of arguments.

Example: the Groovy grape command line tool:

grape resolve [-adhisv] (<groupId> <artifactId> <version>)...

This command can accept repeating groups of groupId, artifactId, and version positional parameters.

Another example could be where a combination of options and positional parameters form a repeating group:

<cmd> (--mode=MODE <file>)...

We would need some way to capture the repeated combination of option and positional parameter in a collection. Instead of having separate collections for each option or positional parameters, I think it makes more sense to model the group itself with a class, and have a collection of this grouping class:

@Command
class App {
    @ArgGroup(exclusive = false, validating = true, multiplicity = "1..*",
              heading = "This is the options list heading (See #450)", order = 1)
    List<MyComposite> compositeArguments;
}

class MyComposite {
    @Option(name = "--mode") Mode mode; // where Mode is an enum
    @Parameters(index = "0") File file;
}

That way, when the command is invoked with repeating composite arguments like this:

<cmd> --mode=A ~/input1 --mode=B ~/input2 --mode=C ~/input3

... the list will contain three MyComposite instances, where each instance has its fields initialized to the command line values specified for its group.

In this new proposed design (yet to be implemented), the required boolean will be replaced with a multiplicity attribute, which allows values like "0..*" for optional repeating groups, "1..*" for mandatory repeating groups, or some specified number of minimum and maximum occurrences. The exclusive, validate, heading and order attributes will still work exactly as before.

In this design, an option or positional parameter can no longer belong to multiple groups. I'm not sure yet if this is a good or a bad thing.

Groups can still be composed, for example:

```java
@Command
class App {
@ArgGroup(exclusive = false, multiplicity = "0..1") CompositeGroup compositeGroup;
}

class CompositeGroup {
@ArgGroup(exclusive = true, multiplicity = "1") ExclusiveGroup exclusiveGroup;
@ArgGroup(exclusive = false, arity = "1") CooccurringGroup cooccurringGroup;
}

class ExclusiveGroup {
@Option(names = "-x") boolean x;
@Option(names = "-y") boolean y;
}

class CooccurringGroup {
@Option(names = "-a") boolean a;
@Option(names = "-b") boolean b;
}
This defines a composite group with the following synopsis:
[(-x | -y) (-a -b)]
````

Thoughts?

Continuing from the previous comment...

Another aspect that the new design will need to cover is programmatic access.

Applications that dynamically generate groups structures, like @bbottema mentions in #358 and #434, should be able to build the model programmatically instead of letting picocli build the model reflectively from a class with annotations.

For example, to build the model for the [(-x | -y) (-a -b)] composite group programmatically could look something like this:

ArgGroupSpec cooccurring = ArgGroupSpec.builder()
    .exclusive(false)
    .multiplicity("1")
    .addOption(OptionSpec.builder("-a").type(boolean.class).build())
    .addOption(OptionSpec.builder("-b").type(boolean.class).build())
    .build();

ArgGroupSpec exclusive = ArgGroupSpec.builder()
    .exclusive(true)
    .multiplicity("1")
    .addOption(OptionSpec.builder("-x").type(boolean.class).build())
    .addOption(OptionSpec.builder("-y").type(boolean.class).build())
    .build();

ArgGroupSpec composite = ArgGroupSpec.builder()
    .exclusive(true)
    .multiplicity("0..1")
    .addGroup(exclusive)
    .addGroup(cooccurring)
    .build();

The application would need to call the CommandLine::parseArgs method and inspect the resulting ParseResult object.
Perhaps the ParseResult class needs to be enhanced with methods to query which group was specified on the command line.

Picocli 4.0.0-alpha-1 has been released which includes support for mutually exclusive options.
See https://picocli.info/#_argument_groups for details.

_Please try this and provide feedback. We can still make changes._

_What do you think of the annotations API? What about the programmatic API? Does it work as expected? Are the input validation error messages correct and clear? Is the documentation clear and complete? Anything you want to change or improve? Any other feedback?_

Picocli 4.0.0-alpha-1 has been released which includes support for mutually exclusive options.
See https://picocli.info/#_argument_groups for details.

_Please try this and provide feedback. We can still make changes._

_What do you think of the annotations API? What about the programmatic API? Does it work as expected? Are the input validation error messages correct and clear? Is the documentation clear and complete? Anything you want to change or improve? Any other feedback?_

Looks, great! I have not yet tried it. However, after reading the docs, I assume, that also something like this would be possible:

    @ArgGroup(exclusive = false, multiplicity = "1..*")
    List<Action> actions;

    static class Action {
        @ArgGroup(exclusive = true, multiplicity = "1")
        ActionContext context;

        @Parameters(index = "0")
        String action;
    }

    static class ActionContext {
        @Option(names = "--context-json")
        Path contextJsonPath;

        @ArgGroup(exclusive = false, multiplicity = "1")
        ManualContext context;
    }

Where ManualContext contains arguments to define action's context using CLI.

Is my understanding correct?

Yes that should work.

@wenhoujx, would you mind creating a separate ticket if you find any issue?

sure

@remkop i was using the library wrong, all good now. thanks!

Was this page helpful?
0 / 5 - 0 ratings