Skip to content

Preprocessor callbacks during parsing (was: Make string options behave same as boolean options) #1217

@remkop

Description

@remkop

From discussion on the picocli google group mailing list, topic raised by @maxandersen:

when I have:

@CommandLine.Option(names = {"--live" })
boolean live;

then jbang --live=true and jbang --live=false works, and jbang --live false is not interpreting the false argument as a possible value. and jbang --live=custom i can even intercept with custom value handler.

now...how can I get the same for

@CommandLine.Option(names = {"--live" })
String live;

with this I cannot force that only --live=myarg gets taken as input and that --live myarg should not be treated as a value to live.

any way for this to work ?

My reply:

The intention was that --option=value and --option value would be treated identically.
There is indeed some special logic for booleans, that takes the separator into account.
This is because boolean options have arity=0, meaning they are not expected to have a parameter.
If the user specified --option=value, then the presence of the separator "forces" us to accept "value" as a parameter, and that is what the logic does.

This is a special case. Generally I would think that treating --option=value and --option value differently would be confusing to users, but there may be other use case that I don't understand yet.

Currently the IParameterConsumer interface does not give any indication of whether the value was attached with a separator or not, that may have been a bad decision.

@maxandersen's reply:

I think it would been great to know if parameter is done via = or not as I really dont see another way of allowing to have options that can have a default beavhior but then be able to specify another option....well you can but that would require having one boolean nand then another flag for the option.

i.e. in my case its jbang where I want to do jbang edit --open myfile.java (which says please make temporary project for myfile.java for me to edit, oh and please open my default editor on that project)
but I also want to be able to write jbang edit --open=idea myfile.java which says the same but says, use the idea command/editor in this case.

There is no real way for me to know wether myfile.java is actually the editor he wants to use or not ....

I've used parameterconsumer in other cases pretty nicely for numeric values where if the value was a number i would acccept it but not if a string/filename that existed - but it just gets dirty.

And when I look at all my special cases I could have handled it much more uniformly if I had a "OnlyExplicitlyAssignedParameterConsumer.class" or something ;)


btw. see jbangdev/jbang#314 for latest issue i'm trying to solve and made me open this one. might give you some more context.

My reply:

Thank you for clarifying the use case: there is an option with an optional parameter, and this introduces ambiguity.

I like your proposed solution to try to disambiguate the input with a '=' separator;
it is intuitive that A is the option parameter in a command like jbang edit --open=A B, where jbang edit --open A B is more ambiguous.
The drawback is that users need to know this.

A similar solution (that already works with all versions of picocli) is to disambiguate with the -- end-of-options delimiter.
jbang edit --open -- A B # both A and B are positional parameters
jbang edit --open A -- B # A is a parameter for the open option and B is a positional parameter

This has the same drawback that users need to know this.
Arguably the -- end-of-options delimiter is less well-known, although it can be shown in the usage help with @Command(showEndOfOptionsDelimiterInUsageHelp=true).
Still, this is something to consider for commands that define both an option with an optional parameter and a positional parameter.

Ultimately, this is a limitation of the picocli parser: if a command has an option with an optional parameter (like --live[=editor]), then the picocli parser will currently greedily assign the value that follows the option to this option. This is regardless of whether the command also defines a positional parameter and the last argument could also have been interpreted as that positional parameter. This is especially painful if the positional parameter is mandatory (see #981).

I raised #980 to improve the parser, but this is not trivial:
it essentially requires the parser to detect ambiguity (all options or positional parameters whose arity is a range instead of a fixed number),
and build up multiple parse results: one for each possible interpretation of the input.
If we succeed in that, we need to decide what to do in cases where

  • all interpretations failed (what error message to produce)
  • multiple interpretations succeeded (which one to choose)

Your idea to disambiguate with '=' will not solve the full range of ambiguity problems, but it may be easier to implement than a backtracking parser.
Currently I can think of two mechanisms to do this:

  1. introduce a new @Option(assignFallbackWhenSeparatorMissing = true|false) attribute.
    For options with an optional parameter, this attribute would tell the parser to use the '=' separator to disambiguate the input.
    The value following the option is only considered to be the option parameter if the value is attached to the option with the '=' separator.
  2. introduce a new IParameterConsumer2 interface, that takes an additional LookBehind { SEPARATE, ATTACHED, ATTACHED_WITH_SEPARATOR} enum parameter to the consumeParameters method.
    We would also need an additional @Option(parameterConsumer2 = <class>) attribute.
    Applications can then use this to build custom disambiguation logic.
  3. Somehow make this information (of whether a separator was specified or not) available to implementors of the existing IParameterConsumer interface.
    Perhaps by adding some method on ArgSpec, CommandSpec or any object reachable from these objects.
    Of by setting a static ThreadLocal. :-)

There may be other approaches, please let me know your thoughts.

@maxandersen's reply

Still, this is something to consider for commands that define both an
option with an optional parameter and a positional parameter.

issue is that it only work for one parameter - what if you have two ?

  1. introduce a new @Option(assignFallbackWhenSeparatorMissing = true|false) attribute. For options with an optional parameter, this attribute would tell the parser to use the '=' separator to disambiguate the input. The value following the option is only considered to be the option parameter if the value is attached to the option with the '=' separator.

+1 for a boolean for its simplicity.

  1. introduce a new IParameterConsumer2 interface, that takes an additional LookBehind { SEPARATE, ATTACHED, ATTACHED_WITH_SEPARATOR} enum parameter to the consumeParameters method. We would also need an additional @Option(parameterConsumer2 = <class>) attribute. Applications can then use this to build custom disambiguation logic.

doesn't feel right.

  1. Somehow make this information (of whether a separator was specified or not) available to implementors of the existing IParameterConsumer interface. Perhaps by adding some method on ArgSpec, CommandSpec or any object reachable from these objects.

better than #2

Of by setting a static ThreadLocal. :-)

noooo! ::)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions