javaparsingcommand-line-argumentspicocli

Picocli required options selection based on a primary option


I would like to parse options with picocli in the following format:

application -mode CLIENT -c aaaa -d bbbb
application -mode SERVER -e xxxx -f yyyy

mode is an enum with values { CLIENT, SERVER }

In other words, I would like to choose the required options based on a key option. Is this possible in picocli?


Solution

  • Yes, this is possible. One way is simple programmatic validation:

    import picocli.CommandLine;
    import picocli.CommandLine.Command;
    import picocli.CommandLine.Model.CommandSpec;
    import picocli.CommandLine.Option;
    import picocli.CommandLine.ParameterException;
    import picocli.CommandLine.Spec;
    
    import java.util.Objects;
    import java.util.function.Predicate;
    
    @Command(name = "application", mixinStandardHelpOptions = true)
    public class MyApp implements Runnable {
    
        enum Mode {CLIENT, SERVER}
    
        @Option(names = "-mode", required = true)
        Mode mode;
    
        @Option(names = "-c") String c;
        @Option(names = "-d") String d;
        @Option(names = "-e") String e;
        @Option(names = "-f") String f;
    
        @Spec CommandSpec spec;
    
        public static void main(String[] args) {
            System.exit(new CommandLine(new MyApp()).execute(args));
        }
    
        @Override
        public void run() {
            validateInput();
            // business logic here...
        }
    
        private void validateInput() {
            String INVALID = "Error: option(s) %s cannot be used in %s mode";
            String REQUIRED = "Error: option(s) %s are required in %s mode";
            if (mode == Mode.CLIENT) {
                check(INVALID, "CLIENT", Objects::isNull, e, "-e", f, "-f");
                check(REQUIRED, "CLIENT", Objects::nonNull, c, "-c", d, "-d");
            } else if (mode == Mode.SERVER) {
                check(INVALID, "SERVER", Objects::isNull, c, "-c", d, "-d");
                check(REQUIRED, "SERVER", Objects::nonNull, e, "-e", f, "-f");
            }
        }
    
        private void check(String msg, String param, Predicate<String> validator, String... valuesAndLabels) {
            String desc = "";
            String sep = "";
            for (int i = 0; i < valuesAndLabels.length; i += 2) {
                if (validator.test(valuesAndLabels[i])) {
                    desc = sep + valuesAndLabels[i + 1];
                    sep = ", ";
                }
            }
            if (desc.length() > 0) {
                throw new ParameterException(spec.commandLine(), String.format(msg, desc, param));
            }
        }
    }
    

    Alternatively, if you are willing to change your requirements a little bit, we can use picocli's argument groups for a more declarative approach:

    import picocli.CommandLine;
    import picocli.CommandLine.ArgGroup;
    import picocli.CommandLine.Command;
    import picocli.CommandLine.Option;
    
    @Command(name = "application", mixinStandardHelpOptions = true)
    public class MyApp2 implements Runnable {
    
        static class ClientArgs {
            @Option(names = "-clientMode", required = true) boolean clientMode;
            @Option(names = "-c", required = true) String c;
            @Option(names = "-d", required = true) String d;
        }
    
        static class ServerArgs {
            @Option(names = "-serverMode", required = true) boolean serverMode;
            @Option(names = "-e", required = true) String e;
            @Option(names = "-f", required = true) String f;
        }
    
        static class Args {
            @ArgGroup(exclusive = false, multiplicity = "1", heading = "CLIENT mode args%n")
            ClientArgs clientArgs;
    
            @ArgGroup(exclusive = false, multiplicity = "1", heading = "SERVER mode args%n")
            ServerArgs serverArgs;
        }
    
        @ArgGroup(exclusive = true, multiplicity = "1")
        Args args;
    
        public static void main(String[] args) {
            System.exit(new CommandLine(new MyApp2()).execute(args));
        }
    
        @Override
        public void run() {
            // business logic here...
        }
    }
    

    When invoked with just -serverMode, this second example will show this error message, followed by the usage help message:

    Error: Missing required argument(s): -e=<e>, -f=<f>
    Usage: application ((-clientMode -c=<c> -d=<d>) | (-serverMode -e=<e> -f=<f>))
    ...
    

    Note that this declarative approach cannot be achieved with a single -mode option: each argument group needs its own option (I chose -clientMode and -serverMode in this example). This allows the picocli parser to figure out which options must occur together and which are mutually exclusive.