Search code examples
javapicocli

PicoCLI: Dependent and Exclusive Arguments mixed


I am trying to achieve something like the following with PicoCLI:

  • Option 0 (help, verbose)
  • Option A
    • Dependent Option A-1
    • Dependent Option A-2
    • Dependent Option A-3
  • Option B
    • Requires Option A
    • But does not allow any Option A-*

I don't know if I can do this setup with PicoCLI tools or if I just check after parsing with custom code.

To this state, Option A is in an ArgGroup where Option A is required, but Optioan A-* not. Option B is in a different ArgGroup. I tried to set some things exclusive, but I can't figure out how to ArgGroup/Exclusive things to work as intended...

Any hints?


Solution

  • To summarize the relationships these options need to have:

    1. -B, -A1, -A2 and -A3 all require the -A option
    2. -B disallows any of the -A1, -A2 and -A3 options
    3. the -A1, -A2 and -A3 options do allow each other
    4. the -A option allows (but does not require) the -B, -A1, -A2 and -A3 options

    The picocli annotations alone will not be sufficient to express all of these relationships declaratively, some custom validation will be necessary.

    So we might as well simplify and create a single argument group, since we cannot express requirement 2 (-B is exclusive with -A1, -A2, -A3) at the same time as requirement 1 and 3 (-B, -A1, -A2 and -A3 all require -A and -A1, -A2, -A3 allow each other).

    A single group like [-A [-B] [-A1] [-A2] [-A3]] will take care of some of the validations: everything except requirement 2 (-B is exclusive with -A1, -A2, -A3). For requirement 2, we need to code some custom validation in the application (example below).

    For your use case it may be useful to have a custom synopsis that accurately reflects the relationships between the options. Something like this:

    Usage: app [-hV] [-A [-B]]
           app [-hV] [-A [-A1] [-A2] [-A3]]
    

    Example code to achieve this:

    import picocli.CommandLine;
    import picocli.CommandLine.*;
    import picocli.CommandLine.Model.CommandSpec;
    
    @Command(name = "app", mixinStandardHelpOptions = true,
            synopsisHeading = "",
            customSynopsis = {
                "Usage: app [-hV] [-A [-B]]",
                "       app [-hV] [-A [-A1] [-A2] [-A3]]",
            })
    public class App implements Runnable {
        static class MyGroup {
            @Option(names = "-A", required = true) boolean a;
            @Option(names = "-B") boolean b;
            @Option(names = "-A1") boolean a1;
            @Option(names = "-A2") boolean a2;
            @Option(names = "-A3") boolean a3;
    
            boolean isInvalid() {
                return b && (a1 || a2 || a3);
            }
        }
    
        @ArgGroup(exclusive = false)
        MyGroup myGroup;
    
        @Spec CommandSpec spec;
    
        public void run() {
            if (myGroup != null && myGroup.isInvalid()) {
                String msg = "Option -B is mutually exclusive with -A1, -A2 and -A3";
                throw new ParameterException(spec.commandLine(), msg);
            }
            System.out.printf("OK: %s%n", spec.commandLine().getParseResult().originalArgs());
        }
    
        public static void main(String[] args) {
            //new CommandLine(new App()).usage(System.out);
    
            //test: these are all valid
            new CommandLine(new App()).execute();
            new CommandLine(new App()).execute("-A -B".split(" "));
    
            // requires validation in the application to disallow
            new CommandLine(new App()).execute("-A -B -A1".split(" "));
    
            // picocli validates this, gives: "Error: Missing required argument(s): -A"
            new CommandLine(new App()).execute("-B -A1".split(" "));
        }
    }