Picocli: Custom handling of positional parameters

Created on 17 Aug 2018  路  8Comments  路  Source: remkop/picocli

I would like to support some rich syntax with my CLI tool. I would like to allow the user to not only pass in a list of integers as positional arguments, but ranges of integers. I also don't want to limit them on how these parameters are mixed together with the others.

Example:

foocli 1 2 4 6 8

Passes in the integer array [1,2,4,6,8] as positional arguments

foocli 1 3-5 2, 8

Passes in the integer array [1, 3, 4 5, 2, 8] as positional arguments.

I would ultimately like to support any range of numbers in any positional argument.

I know this is a rather special-purpose feature and that I can implement it myself using a list of string positional parameters, which I convert to integers myself. Any ideas on how to support it using a mix of picocli features and custom code would be nice.

parser

All 8 comments

Aha! just found the @Unmatched annotation which I will make use of.

Thanks for building such a rich feature set into your tool I knew there had to be a way.

Actually @Unmatched doesn't work quite as I had expected. It seems that when the two below annotations are combined within a Command, I receive a jarring error message instead.

    @CommandLine.Parameters(arity="1..*")
    Set<Integer> viewIds;

    @CommandLine.Unmatched
    List<String> unmatched;
> foo c 2-4
Could not convert '2-4' to Integer for positional parameter at index 0..* (items): java.lang.NumberFormatException: For input string: "2-4"
Usage: cb check [-hV] items...
Check/uncheck task
      items...    List of item identifiers.
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

Seems that my expectation that the field annotated with @Unmatched would be a bucket for all tokens for which errors were encountered was incorrect, so I am reopening the ticket. This is either a bug, or there may be some other mix of features I can learn to use.

I tried setting both of the following flags, just in case, but they do not help.

CommandLine.setUnmatchedArgumentsAllowed(true);
CommandLine.setUnmatchedOptionsArePositionalParams(true);

Let me take a look...

From the parser's point of view, it was able to match the input to a positional parameter: there is a @Parameter field defined that covers position 0 (the position of the first parameter 2-4). So picocli does not consider this an unmatched argument.

@Unmatched will only be populated with unknown options, and positional parameters out of range. For example:

class App {
    @Parameters(index = "0") int first;
    @Parameters(index = "1") int second;
    @Parameters(index = "2") int third;
    @Unmatched String[] remainder;
}

When you give this app the following input: 0 1 2 3 4 it will put "3" and "4" in the remainder field.

For the purposes of your application, since you want to handle input of mixed type (simple scalar values as well as range values) I would simply capture all positional parameters in a String collection and do the type conversion in the application.

Sounds like a good workaround. Is there interest in having what I described as a feature? If so I may take a crack at it, but only if there is consensus that it is needed.

My thoughts:

A separate boolean flag for controlling this feature. When true, it places all arguments which yielded parse errors into the @Unmatched array.

Possibly, having a separate array annotated by @ErrorTokens for errors would be nicer.

I'm thinking the @Unmatched idea was also a workaround to begin with. What you really want is to be able to parse input that is a mixture of simple scalars and range expressions. I had another idea: why not use a custom converter?

class RangeConverter implements ITypeConverter<Set<Integer>> {
    // convert each argument to a set of integers;
    // first parse as scalar, if that fails, parse as range
}

Declare the field as a set of sets:

    @CommandLine.Parameters(arity="1..*")
    Set<Set<Integer>> viewIds;

And flatten the set in the application:

Set<Object> flat = 
    Set.stream()
        .flatMap(Set::stream)
        .collect(Collectors.toSet());

This is much better! Thanks!

Was this page helpful?
0 / 5 - 0 ratings