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.8k stars 414 forks source link

Is it possible to pass Optional.empty() on the command line? #2256

Closed hq6 closed 2 months ago

hq6 commented 2 months ago

Consider the following MWE:

import picocli.CommandLine;
import static picocli.CommandLine.*;
import java.util.*;
import java.io.*;

public class MWE {
  static class Tar {
    @Option(names = { "-d", "--dummy" }, paramLabel = "DUMMY", description = "the test string")
    Optional<Integer> dummy;

    public Tar() {
      dummy = Optional.of(5);
    }
  }

  public static void main(String[] args) throws Exception{
    Tar tar = new Tar();
    System.out.println(tar.dummy);
    new CommandLine(tar).parseArgs(args);
    System.out.println(tar.dummy);
  }
}

If I invoke this as java MWE -d 6, I get the following output, as expected:

Optional[5]
Optional[6]

However, if I try either of the following, I get an exception:

~/Code/PicoPlay java MWE -d Optional.empty
Optional[5]
Exception in thread "main" picocli.CommandLine$ParameterException: Invalid value for option '--dummy': 'Optional.empty' is not an int
        at picocli.CommandLine$Interpreter.tryConvert(CommandLine.java:14795)
        at picocli.CommandLine$Interpreter.applyValueToSingleValuedField(CommandLine.java:14305)
        at picocli.CommandLine$Interpreter.applyOption(CommandLine.java:14180)
        at picocli.CommandLine$Interpreter.processStandaloneOption(CommandLine.java:14045)
        at picocli.CommandLine$Interpreter.processArguments(CommandLine.java:13865)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13591)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13559)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13454)
        at picocli.CommandLine.parseArgs(CommandLine.java:1552)
        at MWE.main(MWE.java:19)
Caused by: picocli.CommandLine$TypeConversionException: 'Optional.empty' is not an int
        at picocli.CommandLine$BuiltIn.fail(CommandLine.java:15143)
        at picocli.CommandLine$BuiltIn$IntegerConverter.convert(CommandLine.java:15155)
        at picocli.CommandLine$BuiltIn$IntegerConverter.convert(CommandLine.java:15154)
        at picocli.CommandLine$Interpreter.tryConvert(CommandLine.java:14792)
        ... 9 more
~/Code/PicoPlay java MWE -d null
Optional[5]
Exception in thread "main" picocli.CommandLine$ParameterException: Invalid value for option '--dummy': 'null' is not an int
        at picocli.CommandLine$Interpreter.tryConvert(CommandLine.java:14795)
        at picocli.CommandLine$Interpreter.applyValueToSingleValuedField(CommandLine.java:14305)
        at picocli.CommandLine$Interpreter.applyOption(CommandLine.java:14180)
        at picocli.CommandLine$Interpreter.processStandaloneOption(CommandLine.java:14045)
        at picocli.CommandLine$Interpreter.processArguments(CommandLine.java:13865)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13591)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13559)
        at picocli.CommandLine$Interpreter.parse(CommandLine.java:13454)
        at picocli.CommandLine.parseArgs(CommandLine.java:1552)
        at MWE.main(MWE.java:19)
Caused by: picocli.CommandLine$TypeConversionException: 'null' is not an int
        at picocli.CommandLine$BuiltIn.fail(CommandLine.java:15143)
        at picocli.CommandLine$BuiltIn$IntegerConverter.convert(CommandLine.java:15155)
        at picocli.CommandLine$BuiltIn$IntegerConverter.convert(CommandLine.java:15154)
        at picocli.CommandLine$Interpreter.tryConvert(CommandLine.java:14792)
        ... 9 more

Is there some magic incantation I can use to pass Optional.empty() on the command line for an Optional argument?

remkop commented 2 months ago

For options that have an Optional<T> type, picocli will convert null values to Optional.empty(). On the command line, users can only pass in Strings (resulting in the String[] args array that is passed to the main method).

The type converters are responsible for converting String values to Java objects. Picocli provides built-in type converters for many common types, including for int and java.lang.Integer. However, the built-in type converter for integer values will never produce a null value, so you need to install a custom type converter that produces the null value for some String argument(s). Then, passing these special values on the command line will result in Optional.empty() values being assigned to the option.

Something like this:

public static main(String... args) {
    CommandLine cmd = new CommandLine(new MWE())
        .registerConverter(Integer.class, s -> new MyNullableIntConverter())
        .registerConverter(Integer.TYPE,  s -> new MyNullableIntConverter());
    cmd.execute(args);
}

static class MyNullableIntConverter implements ITypeConverter<Integer> {
    public Integer convert(String value) throws Exception {
        // make "null" and "Optional.empty" special values that will result in Optional.empty()
        if ("null".equals(value)) { return null; }
        else if ("Optional.empty".equals(value)) { return null; }

        // normal processing for non-special values
        return Integer.valueOf(value);
    }
}
remkop commented 2 months ago

I will close this ticket, feel free to ask more questions or re-open if necessary.