remkop / picocli

Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.
https://picocli.info
Apache License 2.0
4.94k stars 424 forks source link

Mode for treating empty string the same as unspecified #2338

Open nreid260 opened 2 months ago

nreid260 commented 2 months ago

This is similar in spirit to https://github.com/remkop/picocli/issues/1987

Basically, I want to be able to write CLIs such that explicitly passing empty-string as a param value is the same as having not specified the parameter at all. (e.g. --bar=x --foo= is the same as --bar=x). I suggest empty string because it's the closest thing BASH has to as "null" value. I see this as the best practice for optional parameters in any language.

In particular, I want support for default values to work correctly. For example:

@Option(name = "--my_enum") MyEnum myEnum = MyEnum.A
tool --my_enum=

would invoke tool with MyEnum.A as the value for myEnum

My objective is to make it easier to wrappers/scripts/etc to invoke tools with default values, without having to know what the default values are. Wrappers should be able to assume empty-string is the default value for every param.


Obviously this is a backward incompatible change, but could it be added as a setter on CommandLine?

remkop commented 2 months ago

Hi @nreid260 , have you had a chance to read the section on Fallback values (and how they are different from Default values) in the picocli user manual? Hopefully that @fallbackValue annotation is what you are looking for.

If this doesn't meet your requirements, please take a look at the section on Custom Parameter Processing. There's an example use case in the "IParameterPreprocessor Parser Plugin" section that has some similarity to what you are describing, so you may be able to accomplish your use case with that approach.

nreid260 commented 2 months ago

I looked into both of these features and neither of them really capture my intent. Both of them could theoretically achieve the result, but they would be sort of clumbsy.

For fallback values, a string that parses into the desired default is needed. This isn't always possible (e.g. empty list of strings). Moreover, it's repetative, because the fallback value and default value are going to be very similar.

An IParameterPreprocessor could be written that does what I want, but it would need to be installed on every option/param separately. There's no way I can see to configure the entire command to use the same preprocessor. Also, though this isn't important for me, it might conflict with options that need some other preprocessor, besides handling empty-string.

nreid260 commented 2 months ago

Another case has come to mind: options that can be specified multiple times, with last-one-wins semantics. I'm not sure what the right behaviour would be for --foo=x --foo=

  1. Override x with the default value
  2. Ignore --foo= completely, so that x is used
remkop commented 2 months ago

Another case has come to mind: options that can be specified multiple times, with last-one-wins semantics. I'm not sure what the right behaviour would be for --foo=x --foo=

  1. Override x with the default value
  2. Ignore --foo= completely, so that x is used

This can be controlled with CommandLine::setOverwrittenOptionsAllowed (see https://picocli.info/#_overwriting_single_options ).

And in case 1: "Override x" it will be the fallbackValue that is used, not the default value.

remkop commented 2 months ago

I can see how the current situation is not ideal.

Getting it to work is possible but not very elegant. I arrived at something like this:

public class Issue2338 {
    enum MyEnum { A, B, C}

    MyEnum myEnum;
    @CommandLine.Option(names = "--my_enum", defaultValue = "A", fallbackValue = "B", arity = "0..1")
    void setMyEnum(String value) {
        myEnum = MyEnum.valueOf(value.isEmpty() ? "B" : value);
    }
    public static void main(String[] args) {
        Issue2338 case1 = CommandLine.populateCommand(new Issue2338());
        Issue2338 case2 = CommandLine.populateCommand(new Issue2338(), "--my_enum");
        Issue2338 case3 = CommandLine.populateCommand(new Issue2338(), "--my_enum=");
        Issue2338 case4 = CommandLine.populateCommand(new Issue2338(), "--my_enum=C");

        System.out.printf("Option not specified;             myEnum=%s%n", case1.myEnum);
        System.out.printf("Option specified without value;   myEnum=%s%n", case2.myEnum);
        System.out.printf("Option specified as empty string; myEnum=%s%n", case3.myEnum);
        System.out.printf("Option specified with value;      myEnum=%s%n", case4.myEnum);
    }
}

This prints:

Option not specified;             myEnum=A
Option specified without value;   myEnum=B
Option specified as empty string; myEnum=B
Option specified with value;      myEnum=C

So, it is doable, but I can see it would be nice to have a more elegant solution. My time to work on picocli is extremely limited though, and I don't see myself working on this in the near future...