Remko Popma
Remko Popma

Reputation: 36754

Picocli: Best way to specify option with optional value that prints current value when no value specified

I'm writing a REPL (so I'm using picocli internally to parse commands typed within the application, not to parse command line args), and I have a command with an option that I want to behave like this:

> cmd --myopt
Myopt value = 5
> cmd --myopt 4
> cmd --myopt
Myopt value = 4

That is, if the option is specified with no value, the current value of the parameter is printed, but if it is specified with a value, then the value is set. I was thinking to do this like this:

int value = 1; // default
@Option(names = {"-e", "--epsilon"}, arity = "0..1",
        description = "Acceptable values: [0, 1] default: ${DEFAULT-VALUE}")
void setValue(String strValue) {
    if (strValue == "") {
        printValue();
    } else {
        try {
            value = Integer.parseInt(strValue);
            // validate value
        } catch (NumberFormatException e) {
            // print help for this option
        }
    }
}

Is that the best way? Is there another way to capture the default value in the description while still allowing setValue to know that no value was specified?

(See also https://github.com/remkop/picocli/issues/490 )

Upvotes: 1

Views: 4370

Answers (2)

marinier
marinier

Reputation: 509

I actually ended up taking a different approach; it's helpful for my application to be able to directly assign to a field of the actual type (because we're developing a feature where you can "discover" commands that take arguments of various types, so having the field be the actual type makes this reverse lookup easier).

So I ended up doing this:

static class DoubleConverter implements ITypeConverter<Double> {
    public Double convert(String value) throws Exception {
        if(value.isEmpty()) return Double.NaN; // this is a special value that indicates the option was present without a value
        return Double.valueOf(value);
    }
}

@Option(names = {"-e", "--epsilon"}, arity="0..1", description="Acceptable values: [0, 1] default: 0.1", converter=DoubleConverter.class)
Double epsilon;

Basically, I use a converter to store a special value (NaN in this case, because we ended up using a double) to indicate that the option was present without a value (which is different than it not being present at all, in which case it would be null).

Then the validation and other behavior is performed in the run() method as you suggested:

@Override
public void run() {
    // null indicates the option was not present, so do nothing
    if(epsilon != null) {
        // NaN indicates the option was present but with no value, which means we should print the current value
        if(epsilon.equals(Double.NaN)) {
            // print current value from the application
            printEpsilonValue();
        }
        else {
            // validate value
            if(epsilon < 0.0 || epsilon > 1.0) {
                throw new ParameterException(spec.commandLine(), "Invalid parameter value");
            } else {
                // set the value in the application
                setEpsilonValue(episilon);
            }
        }
    }       
}

I wasn't able to specify the default value in the description using the variable, because the actual default value in this case needs to be null. This is a minor sacrifice, though.

I realize this is an unusual case, but it might be nice to support this sort of option (non-boolean with arity 0..n) without the need to resort to special values. Perhaps the ability to specify another field to serve as the boolean value that indicates whether the option was present or not. Then there would be no need for a custom converter, either, and possibly the default could still be specified (i.e., the Double field in this case would get set to the default, but if the option was not present, the corresponding boolean field would be false, so the application would know not to use the value of the Double).

Upvotes: 1

Remko Popma
Remko Popma

Reputation: 36754

That is certainly one way to do it. Bear in mind that the setter method may be called multiple times: once to reset the default values, and then again each time the option is matched on the command line.

An alternative is to change the field type to String, put the @Option annotation on the field, and call this logic (that is in the setValue method above) from your run or call method.

Upvotes: 0

Related Questions