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.93k stars 425 forks source link

Generic converter/completion candidates iterator for enum values? #2025

Open rsenden opened 1 year ago

rsenden commented 1 year ago

Our application provides various CLI options taking enum values. Some of these enums have values consisting of multiple words like single line, which we'd like users to enter as single-line. Obviously, we can't use - when defining enum values, so for this example we define an enum value single_line and use a custom converter and completion candidates iterator to convert between _ and -. Currently we need to create a custom converter and iterator class for every enum, and explicitly specify these in the Option definition, for example:

public final class ProgressWriterTypeConverter implements ITypeConverter<ProgressWriterType> {
    @Override
    public ProgressWriterType convert(String value) throws Exception {
        return ProgressWriterType.valueOf(value.replace('-', '_'));
    }

    public static final class ProgressWriterTypeIterable extends ArrayList<String> {
        private static final long serialVersionUID = 1L;
        public ProgressWriterTypeIterable() { 
            super(Stream.of(ProgressWriterType.values())
                    .map(Enum::name)
                    .map(s->s.replace('_', '-'))
                    .collect(Collectors.toList())); 
        }
    }
}
@Option(names="--progress", defaultValue = "auto", completionCandidates = ProgressWriterTypeIterable.class, converter = ProgressWriterTypeConverter.class ) 
    private ProgressWriterType type;

Also, as a best practice, enum values should be uppercase, and although picocli optionally allows for case-insensitive matching, we'd like to have the help output display completion candidates in lowercase; again we'd need to define a custom completion candidates iterator on every option that takes an enum value. For now, we avoid this by just using lowercase enum values.

I think it would be nice if picocli allowed for registering one or more generic custom type converters and completion candidate suppliers for arbitrary types (which could be enums or other custom types), based on interfaces like the following:

public interface IGenericTypeConverter {
    boolean canConvert(Class<?> optionFieldType);
    <T> T convert(String value, Class<T> optionFieldType);
}
public interface IGenericCompletionCandidatesSupplier {
    boolean canGenerateCompletionCandidates(Class<?> optionFieldType);
    Iterator/Collection/... getCompletionCandidates(Class<?> optionFieldType);
}

Implementations of these interfaces could then be registered on CommandLine or CommandSpec. Then, whenever picocli needs to convert an input string to store it into an option field, and the option doesn't explicitly specify a converter, picocli would iterate over the registered IGenericTypeConverter instances (either in reverse order of addition, or by allowing explicit ordering) to find the first one for which canConvert returns true, then calling the convert method to get the converted value. Obviously, same procedure would apply when generating completion candidates.

With this approach, we could register a GenericEnumTypeConverter like the following, handling both case-insensitivity and converting - to _, without having to explicitly define converter and completionCandidates on every enum option:

    public static class GenericEnumTypeConverter implements IGenericTypeConverter {
        @Override
        public boolean canConvert(Class<?> optionFieldType) {
            return Enum.class.isAssignableFrom(optionFieldType);
        }
        @Override
        public <T> T convert(String value, Class<T> optionFieldType) {
            return convertEnumValue(value.toUpperCase().replace('-', '_'), optionFieldType);
        }
        @SuppressWarnings({ "rawtypes", "unchecked" })
        private <T> T convertEnumValue(String value, Class type) {
            return (T)Enum.valueOf(type, value);
        }
    }
remkop commented 1 year ago

Potentially related: https://github.com/remkop/picocli/issues/1804