Search code examples
javacommand-line-argumentspicocli

Picocli: Is it possible to define options with a space in the name?


I googled around for a bit and also searched on StackOverflow and of course the Picocli docs but didn't come to any solution.

The company I work at uses a special format for command line parameters in batch programs:

-VAR ARGUMENT1=VALUE -VAR ARGUMENT2=VALUE2 -VAR BOOLEANARG=FALSE

(Don't ask me why this format is used, I already questioned it and didn't get a proper answer.) Now I wanted to use Picocli for command line parsing. However, I can't get it to work with the parameter format we use, because the space makes Picocli think those are two separate arguments and thus it won't recognise them as the ones I defined.

This won't work, obviously:

@CommandLine.Option( names = { "-VAR BOOLEANARG" } )
boolean booleanarg = true;

Calling the program with -VAR BOOLEANARG=FALSE won't have any effect.

Is there any way to custom define those special option names containing spaces? Or how would I go about it? I also am not allowed to collapse multiple arguments as parameters into one -VAR option.

Help is much appreciated. Thanks and best regards, Rosa


Solution

  • Solution 1: Map Option

    The simplest solution is to make -VAR a Map option. That could look something like this:

    @Command(separator = " ")
    class Simple implements Runnable {
    
        enum MyOption {ARGUMENT1, OTHERARG, BOOLEANARG}
    
        @Option(names = "-VAR",
                description = "Variable options. Valid keys: ${COMPLETION-CANDIDATES}.")
        Map<MyOption, String> options;
    
        @Override
        public void run() {
            // business logic here
        }
    
        public static void main(String[] args) {
            new CommandLine(new Simple()).execute(args);
        }
    }
    

    The usage help for this example would look like this:

    Usage: <main class> [-VAR <MyOption=String>]...
          -VAR <MyOption=String>
             Variable options. Valid keys: ARGUMENT1, OTHERARG, BOOLEANARG.
    

    Note that with this solution all values would have the same type (String in this example), and you may need to convert to the desired type (boolean, int, other...) in the application.

    However, this may not be acceptable given this sentence in your post:

    I also am not allowed to collapse multiple arguments as parameters into one -VAR option.

    Solution 2: Argument Groups

    One idea for an alternative is to use argument groups: we can make ARGUMENT1, OTHERARG, and BOOLEANARG separate options, and put them in a group so that they must be preceded by the -VAR option.

    The resulting usage help looks something like this:

    Usage: group-demo [-VAR (ARGUMENT1=<arg1> | OTHERARG=<otherValue> |
                      BOOLEANARG=<bool>)]... [-hV]
          -VAR                   Option prefix. Must be followed by one of
                                   ARGUMENT1, OTHERARG or BOOLEANARG
          ARGUMENT1=<arg1>       An arg. Must be preceded by -VAR.
          OTHERARG=<otherValue>  Another arg. Must be preceded by -VAR.
          BOOLEANARG=<bool>      A boolean arg. Must be preceded by -VAR.
      -h, --help                 Show this help message and exit.
      -V, --version              Print version information and exit.
    

    And the implementation could look something like this:

    @Command(name = "group-demo", mixinStandardHelpOptions = true,
            sortOptions = false)
    class UsingGroups implements Runnable {
    
        static class MyGroup {
            @Option(names = "-VAR", required = true,
              description = "Option prefix. Must be followed by one of ARGUMENT1, OTHERARG or BOOLEANARG")
            boolean ignored;
    
            static class InnerGroup {
                @Option(names = "ARGUMENT1", description = "An arg. Must be preceded by -VAR.")
                String arg1;
    
                @Option(names = "OTHERARG", description = "Another arg. Must be preceded by -VAR.")
                String otherValue;
    
                @Option(names = "BOOLEANARG", arity = "1",
                  description = "A boolean arg. Must be preceded by -VAR.")
                Boolean bool;
            }
    
            // exclusive: only one of these options can follow a -VAR option
            // multiplicity=1: InnerGroup must occur once
            @ArgGroup(multiplicity = "1", exclusive = true)
            InnerGroup inner;
        }
    
    
        // non-exclusive means co-occurring, so if -VAR is specified,
        // then it must be followed by one of the InnerGroup options
        @ArgGroup(multiplicity = "0..*", exclusive = false)
        List<MyGroup> groupOccurrences;
    
        @Override
        public void run() {
            // business logic here
    
            System.out.printf("You specified %d -VAR options.%n", groupOccurrences.size());
            for (MyGroup group : groupOccurrences) {
                System.out.printf("ARGUMENT1=%s, ARGUMENT2=%s, BOOLEANARG=%s%n",
                        group.inner.arg1, group.inner.arg2, group.inner.arg3);
            }
        }
    
        public static void main(String[] args) {
            new CommandLine(new UsingGroups()).execute(args);
        }
    }
    

    Then, invoking with java UsingGroups -VAR ARGUMENT1=abc -VAR BOOLEANARG=true gives:

    You specified 2 -VAR options.
    ARGUMENT1=abc, OTHERARG=null, BOOLEANARG=null
    ARGUMENT1=null, OTHERARG=null, BOOLEANARG=true
    

    With this approach, you will get a MyGroup object for every time the end user specifies -VAR. This MyGroup object has an InnerGroup which has many fields, all but one of which will be null. Only the field that the user specified will be non-null. That is the disadvantage of this approach: in the application you would need to inspect all fields to find the non-null one that the user specified. The benefit is that by selecting the right type for the @Option-annotated field, the values will be automatically converted to the destination type.