Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 }

  • 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?

like image 851
TFuto Avatar asked Sep 05 '25 07:09

TFuto


1 Answers

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.

like image 87
Remko Popma Avatar answered Sep 07 '25 19:09

Remko Popma