3

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 }

  • If mode == CLIENT, -c and -d options are mandatory, and -e, -f must not be used.
  • If mode == SERVER, -e and -f options are mandatory, and -c, -d must not be used.

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

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
TFuto
  • 1,361
  • 15
  • 33

1 Answers1

4

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.

Remko Popma
  • 35,130
  • 11
  • 92
  • 114
  • @Remko The above declarative approach is not working for sub-commands. Do you have any suggestions to make it work for sub-commands? – Ankit Jan 29 '21 at 11:14
  • @Ankit Would you mind raising a ticket on the picocli issue tracker on GitHub? Ideally with sample code to reproduce the issue and what error you are seeing (or what you expected to happen and what actually happens)? – Remko Popma Jan 29 '21 at 11:58
  • @RemkoPopma The issue was in my code and it is fixed now, commands are working as expected. Thanks. – Ankit Feb 01 '21 at 19:51
  • @Ankit Excellent! Enjoy! – Remko Popma Feb 01 '21 at 21:44
  • I have used your "-clientMode" and "-serverMode" approach. Is it somehow possible to make for example "-serverMode" default one? i.e., when I do not pass any mode, but I will pass "-e" and "-f" parameters, ServerArgs will be created and ready to use. – Lubo Apr 03 '21 at 13:16
  • @Lubo What if you make -serverMode non-required? – Remko Popma Apr 03 '21 at 17:36
  • @RemkoPopma, does not make sense... After proposed change (Option on boolean serverMode with required=false), I get an error like this: `Error: Missing required argument (specify one of these): ((-clientMode) | ([-serverMode] [-x=])) ` But on other side, when I use optional parameter -x from server mode static class, it automatically goes right way (creating ServerArgs instance, without exception). I have perhaps too different problem to solve. I will ask separate question after some tries. Thanks. – Lubo Apr 04 '21 at 21:14
  • In the example of the question, I would _only_ make `-serverMode` non-required, but I would leave the `-e` and `-f` options as required. But perhaps your use case is different enough to warrant a separate question. – Remko Popma Apr 04 '21 at 21:17