Search code examples
picocli

Why is picocli requiring args within an ArgGroup even with the default multiplicity of 0..1?


You can find an example of the code here. Link to my GitHub project

In the file Driver.java, you can see that I have specified an exclusive ArgGroup. My understanding based on the documentation is that the default multiplicity is 0..1. The documentation states, "The default is multiplicity = "0..1", meaning that by default a group may be omitted or specified once".

I have also tried to set the multiplicity explicitly to 0..1 but that did not change the behavior. Running the program without either the -al or -rl options, the parsing throws a NullPointerException. The framework is behaving as though one of those options is required. That is not consistent with the documentation. I should be able to run this program with only the -n option if I want. I want the ArgGroup to be completely optional.

The program at the git hub link is a fully functioning maven project that one could clone, build, and run. However here is the stack trace. With no arguments specified or without the arg group. I expect that with no arguments at all that the usage info would be printed. Also the default multiplicity for the group is supposed to be 0..1 so I shouldn't have to specify one of the options within the arg group.

java.lang.NullPointerException
    at com.shawnfox.java4.concurrency.Driver.call(Driver.java:58)
    at com.shawnfox.java4.concurrency.Driver.call(Driver.java:1)
    at picocli.CommandLine.executeUserObject(CommandLine.java:1743)
    at picocli.CommandLine.access$900(CommandLine.java:145)
    at picocli.CommandLine$RunLast.handle(CommandLine.java:2101)
    at picocli.CommandLine$RunLast.handle(CommandLine.java:2068)
    at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:1935)
    at picocli.CommandLine.execute(CommandLine.java:1864)
    at com.shawnfox.java4.concurrency.Driver.main(Driver.java:50)

Solution

  • Thank you for adding the stack trace. I see that the NullPointerException occurs in the call method on line 58, not in picocli itself.

    So the problem is not that picocli requires options in an optional (multiplicity = 0..1) argument group, the problem is that the call method assumes that the @ArgGroup-annotated field will always be initialized, even if no option in the group is matched. This assumption is incorrect.

    What happens is, if neither the -al option nor the -rl option are specified on the command line, then there is no match for the SynchronizationOptions argument group at all, so picocli will not instantiate a SynchronizationOptions object and the synchOptions field on line 32 will not be initialized.

    This is how the picocli parser works with argument groups: for a group with multiplicity *, for example, picocli will create an instance of the user object for every group match and add it to the annotated collection/array field.

    If no group is matched, there will be zero instances of the user object. This allows the application to detect exactly whether the group was matched or not - and if the group is matched, the application can rely on the invariant that for an exclusive group only one option was matched and has a value, and for a co-occurring group all options were matched and have values from the command line. (This would not be possible if picocli instantiated the user object without a match.)

    The solution is to change the application to either check for null or initialize the synchOptions field in the application. The latter is probably simplest and cleanest. For example, replace:

    @ArgGroup(exclusive = true)
    SynchronizationOptions synchOptions;
    

    with

    @ArgGroup(exclusive = true)
    SynchronizationOptions synchOptions = new SynchronizationOptions();
    

    Then synchOptions is never null so the application can safely reference its fields in the call method:

    public Void call() {
        if (synchOptions.useReentrantLock) {
            // ...
    

    Alternatively, check whether synchOptions == null in the call method. This allows the application to detect whether any of the synch options was matched, and if it was matched, the application can rely on the fact that at least one of the boolean fields is true.