diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a9c5f70b..385a0361 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -23,9 +23,10 @@ Picocli follows [semantic versioning](http://semver.org/). ## New and Noteworthy ## Fixed issues +* [#1241] API: Add `mapFallbackValue` attribute to `@Options` and `@Parameters` annotations, and corresponding `ArgSpec.mapFallbackValue()`. * [#1184] API: Added public methods `Help.Layout::colorScheme`, `Help.Layout::textTable`, `Help.Layout::optionRenderer`, `Help.Layout::parameterRenderer`, and `Help::calcLongOptionColumnWidth`. * [#1108] Enhancement: Support `Optional` type for options and positional parameters. Thanks to [Max Rydahl Andersen](https://github.com/maxandersen) for raising this. -* [#1214] Enhancement: Support Map options with key-only (support `-Dkey` as well as `-Dkey=value`). Thanks to [Max Rydahl Andersen](https://github.com/maxandersen) for raising this. +* [#1214] Enhancement: Support Map options with key-only (support `-Dkey` as well as `-Dkey=value`). Thanks to [Max Rydahl Andersen](https://github.com/maxandersen) and [David Walluck](https://github.com/dwalluck) for raising this and subsequent discussion. * [#1236] Enhancement/bugfix: Fix compiler warnings about `Annotation::getClass` and assignment in `if` condition. Thanks to [nveeser-google](https://github.com/nveeser-google) for the pull request. * [#1229] Bugfix: Fix compilation error introduced with fc5ef6de6 (#1184). Thanks to [Andreas Deininger](https://github.com/deining) for the pull request. * [#1225] Bugfix: Error message for unmatched positional argument reports incorrect index when value equals a previously matched argument. Thanks to [Vitaly Shukela](https://github.com/vi) for raising this. @@ -47,7 +48,7 @@ Picocli follows [semantic versioning](http://semver.org/). No features were deprecated in this release. ## Potential breaking changes -This release has no breaking changes. +Added method `isOptional()` to the `picocli.CommandLine.Model.ITypeInfo` interface. # Picocli 4.5.2 diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/CompileTimeTypeInfo.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/CompileTimeTypeInfo.java index 2529b6cd..c8ebb453 100644 --- a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/CompileTimeTypeInfo.java +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/CompileTimeTypeInfo.java @@ -153,6 +153,12 @@ class CompileTimeTypeInfo implements CommandLine.Model.ITypeInfo { return type.getKind() == TypeKind.BOOLEAN || "java.lang.Boolean".equals(type.toString()); } + @Override + public boolean isOptional() { + TypeMirror type = auxTypeMirrors.get(0); + return "java.util.Optional".equals(type.toString()); + } + @Override public boolean isMultiValue() { return isArray() || isCollection() || isMap(); diff --git a/picocli-examples/src/test/java/picocli/MapOptionsOptionalTest.java b/picocli-examples/src/test/java/picocli/MapOptionsOptionalTest.java index 19a70e4d..f31a0aeb 100644 --- a/picocli-examples/src/test/java/picocli/MapOptionsOptionalTest.java +++ b/picocli-examples/src/test/java/picocli/MapOptionsOptionalTest.java @@ -32,7 +32,7 @@ public class MapOptionsOptionalTest { @Test public void testOptionalIfNoValue() { class App { - @Option(names = "-D") Map> map; + @Option(names = "-D", mapFallbackValue = "") Map> map; } App app = CommandLine.populateCommand(new App(), "-Dkey"); assertEquals(1, app.map.size()); @@ -42,7 +42,7 @@ public class MapOptionsOptionalTest { @Test public void testOptionalEmptyIfNoValueWithFallbackNull() { class App { - @Option(names = "-D", fallbackValue = "_NULL_") Map> map; + @Option(names = "-D", mapFallbackValue = "_NULL_") Map> map; } App app = CommandLine.populateCommand(new App(), "-Dkey"); assertEquals(1, app.map.size()); @@ -62,7 +62,7 @@ public class MapOptionsOptionalTest { @Test public void testOptionalIfNoValueMultiple() { class App { - @Option(names = "-D") Map> map; + @Option(names = "-D", mapFallbackValue = "") Map> map; } App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2"); assertEquals(2, app.map.size()); @@ -73,7 +73,7 @@ public class MapOptionsOptionalTest { @Test public void testOptionalIfNoValueMultipleWithFallbackNull() { class App { - @Option(names = "-D", fallbackValue = "_NULL_") Map> map; + @Option(names = "-D", mapFallbackValue = "_NULL_") Map> map; } App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2"); assertEquals(2, app.map.size()); @@ -94,7 +94,7 @@ public class MapOptionsOptionalTest { @Test public void testBooleanIfNoValueMultiple() { class App { - @Option(names = "-E", fallbackValue = "true") Map map; + @Option(names = "-E", mapFallbackValue = "true") Map map; } App app = CommandLine.populateCommand(new App(), "-Ekey1", "-Ekey2"); assertEquals(2, app.map.size()); @@ -105,7 +105,7 @@ public class MapOptionsOptionalTest { @Test public void testOptionalIntegerIfNoValueMultiple() { class App { - @Option(names = "-D", fallbackValue = "_NULL_") Map> map; + @Option(names = "-D", mapFallbackValue = "_NULL_") Map> map; } App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2"); assertEquals(2, app.map.size()); diff --git a/picocli-examples/src/test/java/picocli/OptionalTest.java b/picocli-examples/src/test/java/picocli/OptionalTest.java index abe02e80..7f60585e 100644 --- a/picocli-examples/src/test/java/picocli/OptionalTest.java +++ b/picocli-examples/src/test/java/picocli/OptionalTest.java @@ -1,6 +1,8 @@ package picocli; import org.junit.Test; import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -30,6 +32,22 @@ public class OptionalTest { Optional positional = Optional.empty(); } + @Test + public void testTypeInfo() { + CommandSpec spec = CommandSpec.forAnnotatedObject(new SingleOptions()); + OptionSpec x = spec.findOption("-x"); + assertTrue(x.typeInfo().isOptional()); + assertEquals(Optional.class, x.typeInfo().getType()); + assertEquals(1, x.typeInfo().getAuxiliaryTypes().length); + assertEquals(Integer.class, x.typeInfo().getAuxiliaryTypes()[0]); + + OptionSpec z = spec.findOption("-z"); + assertTrue(z.typeInfo().isOptional()); + assertEquals(Optional.class, z.typeInfo().getType()); + assertEquals(1, z.typeInfo().getAuxiliaryTypes().length); + assertEquals(Boolean.class, z.typeInfo().getAuxiliaryTypes()[0]); + } + @Test public void testOptionalSingleOptions() { SingleOptions bean = CommandLine.populateCommand(new SingleOptions(), diff --git a/picocli-groovy/src/main/java/picocli/groovy/PicocliBaseScript2.java b/picocli-groovy/src/main/java/picocli/groovy/PicocliBaseScript2.java index 75ba11c6..8031b4ad 100644 --- a/picocli-groovy/src/main/java/picocli/groovy/PicocliBaseScript2.java +++ b/picocli-groovy/src/main/java/picocli/groovy/PicocliBaseScript2.java @@ -4,6 +4,8 @@ import groovy.lang.GroovyRuntimeException; import groovy.lang.MissingPropertyException; import groovy.lang.Script; import picocli.CommandLine; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; import picocli.CommandLine.IExecutionExceptionHandler; import picocli.CommandLine.IParameterExceptionHandler; import picocli.CommandLine.ParameterException; diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index c0feb63e..b4391e36 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -3496,7 +3496,7 @@ public class CommandLine { private static boolean isBoolean(Class type) { return type == Boolean.class || type == Boolean.TYPE; } private static CommandLine toCommandLine(Object obj, IFactory factory) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj, factory);} private static boolean isMultiValue(Class cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); } - private static boolean isOptional(Class cls) { return "java.util.Optional".equals(cls.getName()); } // #1108 + private static boolean isOptional(Class cls) { return cls != null && "java.util.Optional".equals(cls.getName()); } // #1108 private static Object getOptionalEmpty() throws Exception { return Class.forName("java.util.Optional").getMethod("empty").invoke(null); } @@ -3922,6 +3922,16 @@ public class CommandLine { * @since 4.0 */ String fallbackValue() default ""; + /** For options of type Map, setting the {@code mapFallbackValue} to any value allows end user + * to specify key-only parameters for this option. For example, {@code -Dkey} instead of {@code -Dkey=value}. + *

The value specified in this annotation is the value that is put into the Map for the user-specified key. + * Use the special value {@link ArgSpec#NULL_VALUE} to specify {@code null}.

+ *

If no {@code mapFallbackValue} is set, key-only Map parameters like {@code -Dkey} + * are considered invalid user input and cause a {@link ParameterException} to be thrown.

+ * @see ArgSpec#mapFallbackValue() + * @since 4.6 */ + String mapFallbackValue() default ArgSpec.UNSPECIFIED; + /** * Optionally specify a custom {@code IParameterConsumer} to temporarily suspend picocli's parsing logic * and process one or more command line arguments in a custom manner. @@ -4130,6 +4140,16 @@ public class CommandLine { * and process one or more command line arguments in a custom manner. * @since 4.0 */ Class parameterConsumer() default NullParameterConsumer.class; + + /** For positional parameters of type Map, setting the {@code mapFallbackValue} to any value allows end user + * to specify key-only parameters for this parameter. For example, {@code key} instead of {@code key=value}. + *

The value specified in this annotation is the value that is put into the Map for the user-specified key. + * Use the special value {@link ArgSpec#NULL_VALUE} to specify {@code null}.

+ *

If no {@code mapFallbackValue} is set, key-only Map parameters like {@code -Dkey} + * are considered invalid user input and cause a {@link ParameterException} to be thrown.

+ * @see ArgSpec#mapFallbackValue() + * @since 4.6 */ + String mapFallbackValue() default ArgSpec.UNSPECIFIED; } /** @@ -6226,7 +6246,7 @@ public class CommandLine { private void addOptionNegative(OptionSpec option, Tracer tracer) { if (option.negatable()) { - if (!option.typeInfo().isBoolean() && !isOptional(option.type())) { // #1108 + if (!option.typeInfo().isBoolean() && !option.typeInfo().isOptional()) { // #1108 throw new InitializationException("Only boolean options can be negatable, but " + option + " is of type " + option.typeInfo().getClassName()); } for (String name : interpolator.interpolate(option.names())) { // cannot be null or empty @@ -8025,10 +8045,12 @@ public class CommandLine { /** Models the shared attributes of {@link OptionSpec} and {@link PositionalParamSpec}. * @since 3.0 */ public abstract static class ArgSpec { + public static final String NULL_VALUE = "_NULL_"; static final String DESCRIPTION_VARIABLE_DEFAULT_VALUE = "${DEFAULT-VALUE}"; static final String DESCRIPTION_VARIABLE_FALLBACK_VALUE = "${FALLBACK-VALUE}"; static final String DESCRIPTION_VARIABLE_COMPLETION_CANDIDATES = "${COMPLETION-CANDIDATES}"; private static final String NO_DEFAULT_VALUE = "__no_default_value__"; + private static final String UNSPECIFIED = "__unspecified__"; private final boolean inherited; @@ -8053,6 +8075,7 @@ public class CommandLine { private final ITypeConverter[] converters; private final Iterable completionCandidates; private final IParameterConsumer parameterConsumer; + private final String mapFallbackValue; private final String defaultValue; private Object initialValue; private final boolean hasInitialValue; @@ -8095,6 +8118,7 @@ public class CommandLine { setter = builder.setter; scope = builder.scope; scopeType = builder.scopeType; + mapFallbackValue = builder.mapFallbackValue; Range tempArity = builder.arity; if (tempArity == null) { @@ -8279,6 +8303,19 @@ public class CommandLine { * @since 4.0 */ public Object userObject() { return userObject; } + /** Returns the fallback value for this Map option or positional parameter: the value that is put into the Map when only the + * key is specified for the option or positional parameter, like {@code -Dkey} instead of {@code -Dkey=value}. + *

If the special value {@link #NULL_VALUE} is set on the builder, the {@code ArgSpec.mapFallbackValue()} getter returns {@code null}.

+ *

If no {@code mapFallbackValue} is set, key-only Map parameters like {@code -Dkey} + * are considered invalid user input and cause a {@link ParameterException} to be thrown.

+ * @see Option#mapFallbackValue() + * @see Parameters#mapFallbackValue() + * @since 4.6 */ + public String mapFallbackValue() { + String result = interpolate(mapFallbackValue); + return NULL_VALUE.equals(result) ? null : result; + } + /** Returns the default value to assign if this option or positional parameter was not specified on the command line, before splitting and type conversion. * This method returns the programmatically set value; this may differ from the default value that is actually used: * if this ArgSpec is part of a CommandSpec with a {@link IDefaultValueProvider}, picocli will first try to obtain @@ -8559,6 +8596,7 @@ public class CommandLine { protected boolean equalsImpl(ArgSpec other) { return Assert.equals(this.defaultValue, other.defaultValue) + && Assert.equals(this.mapFallbackValue, other.mapFallbackValue) && Assert.equals(this.arity, other.arity) && Assert.equals(this.hidden, other.hidden) && Assert.equals(this.inherited, other.inherited) @@ -8576,6 +8614,7 @@ public class CommandLine { protected int hashCodeImpl() { return 17 + 37 * Assert.hashCode(defaultValue) + + 37 * Assert.hashCode(mapFallbackValue) + 37 * Assert.hashCode(arity) + 37 * Assert.hashCode(hidden) + 37 * Assert.hashCode(inherited) @@ -8662,6 +8701,7 @@ public class CommandLine { private IScope scope = new ObjectScope(null); private ScopeType scopeType = ScopeType.LOCAL; private IAnnotatedElement annotatedElement; + private String mapFallbackValue = UNSPECIFIED; Builder() {} Builder(ArgSpec original) { @@ -8692,6 +8732,7 @@ public class CommandLine { setter = original.setter; scope = original.scope; scopeType = original.scopeType; + mapFallbackValue = original.mapFallbackValue; } Builder(IAnnotatedElement annotatedElement) { this.annotatedElement = annotatedElement; @@ -8721,6 +8762,7 @@ public class CommandLine { splitRegexSynopsisLabel = option.splitSynopsisLabel(); hidden = option.hidden(); defaultValue = option.defaultValue(); + mapFallbackValue = option.mapFallbackValue(); showDefaultValue = option.showDefaultValue(); scopeType = option.scope(); inherited = false; @@ -8753,6 +8795,7 @@ public class CommandLine { splitRegexSynopsisLabel = parameters.splitSynopsisLabel(); hidden = parameters.hidden(); defaultValue = parameters.defaultValue(); + mapFallbackValue = parameters.mapFallbackValue(); showDefaultValue = parameters.showDefaultValue(); scopeType = parameters.scope(); inherited = false; @@ -8854,6 +8897,16 @@ public class CommandLine { * @since 4.0 */ public Object userObject() { return userObject; } + /** Returns the fallback value for this Map option or positional parameter: the value that is put into the Map when only the + * key is specified for the option or positional parameter, like {@code -Dkey} instead of {@code -Dkey=value}. + *

If the special value {@link #NULL_VALUE} is set on the builder, the {@code ArgSpec.mapFallbackValue()} getter returns {@code null}.

+ *

If no {@code mapFallbackValue} is set, key-only Map parameters like {@code -Dkey} + * are considered invalid user input and cause a {@link ParameterException} to be thrown.

+ * @see Option#mapFallbackValue() + * @see Parameters#mapFallbackValue() + * @since 4.6 */ + public String mapFallbackValue() { return mapFallbackValue; } + /** Returns the default value of this option or positional parameter, before splitting and type conversion. * A value of {@code null} means this option or positional parameter does not have a default. */ public String defaultValue() { return defaultValue; } @@ -8975,6 +9028,16 @@ public class CommandLine { * @since 4.0 */ public T userObject(Object userObject) { this.userObject = Assert.notNull(userObject, "userObject"); return self(); } + /** Sets the fallback value for this Map option or positional parameter: the value that is put into the Map when only the + * key is specified for the option or positional parameter, like {@code -Dkey} instead of {@code -Dkey=value}. + *

Setting the special value {@link #NULL_VALUE}, will cause the getter method to return {@code null}.

+ *

If no {@code mapFallbackValue} is set, key-only Map parameters like {@code -Dkey} + * are considered invalid user input and cause a {@link ParameterException} to be thrown.

+ * @see Option#mapFallbackValue() + * @see Parameters#mapFallbackValue() + * @since 4.6 */ + public Builder mapFallbackValue(String fallbackValue) { this.mapFallbackValue = fallbackValue; return self(); } + /** Sets the default value of this option or positional parameter to the specified value, and returns this builder. * Before parsing the command line, the result of {@linkplain #splitRegex() splitting} and {@linkplain #converters() type converting} * this default value is applied to the option or positional parameter. A value of {@code null} or {@code "__no_default_value__"} means no default. */ @@ -9152,12 +9215,13 @@ public class CommandLine { /** Returns the fallback value for this option: the value that is assigned for options with an optional parameter * (for example, {@code arity = "0..1"}) if the option was specified on the command line without parameter. + *

If the special value {@link #NULL_VALUE} is set, this method returns {@code null}.

* @see Option#fallbackValue() * @see #defaultValue() * @since 4.0 */ public String fallbackValue() { String result = interpolate(fallbackValue); - return "_NULL_".equals(result) ? null : result; + return NULL_VALUE.equals(result) ? null : result; } public boolean equals(Object obj) { @@ -9249,6 +9313,7 @@ public class CommandLine { /** Returns the fallback value for this option: the value that is assigned for options with an optional * parameter if the option was specified on the command line without parameter. + *

If the special value {@link #NULL_VALUE} is set on the builder, the {@code OptionSpec.fallbackValue()} getter returns {@code null}.

* @see Option#fallbackValue() * @since 4.0 */ public String fallbackValue() { return fallbackValue; } @@ -9278,6 +9343,7 @@ public class CommandLine { /** Sets the fallback value for this option: the value that is assigned for options with an optional * parameter if the option was specified on the command line without parameter, and returns this builder. + *

Setting the special value {@link #NULL_VALUE}, will cause the getter method to return {@code null}.

* @see Option#fallbackValue() * @since 4.0 */ public Builder fallbackValue(String fallbackValue) { this.fallbackValue = fallbackValue; return self(); } @@ -10143,6 +10209,10 @@ public class CommandLine { boolean isBoolean(); /** Returns {@code true} if {@link #getType()} is an array, map or collection. */ boolean isMultiValue(); + + /** Returns {@code true} if {@link #getType()} is {@code java.util.Optional} + * @since 4.6 */ + boolean isOptional(); boolean isArray(); boolean isCollection(); boolean isMap(); @@ -12554,7 +12624,7 @@ public class CommandLine { Range arity = arg.arity().min(Math.max(1, arg.arity().min)); applyOption(arg, false, LookBehind.SEPARATE, false, arity, stack(defaultValue), new HashSet(), arg.toString); } else { - if (isOptional(arg.type())) { + if (arg.typeInfo().isOptional()) { if (tracer.isDebug()) {tracer.debug("Applying Optional.empty() to %s on %s%n", arg, arg.scopeString());} arg.setValue(getOptionalEmpty()); } else { @@ -13034,7 +13104,7 @@ public class CommandLine { if (!lookBehind.isAttached()) { parseResultBuilder.nowProcessing(argSpec, value); } // update position for Completers } if (noMoreValues && actualValue == null && interactiveValue == null) { - if (isOptional(argSpec.type())) { + if (argSpec.typeInfo().isOptional()) { if (tracer.isDebug()) {tracer.debug("Applying Optional.empty() to %s on %s%n", argSpec, argSpec.scopeString());} argSpec.setValue(getOptionalEmpty()); } @@ -13057,9 +13127,6 @@ public class CommandLine { } else { actualValue = "***"; // mask interactive value } - if (isOptional(argSpec.type())) { - newValue = getOptionalOfNullable(newValue); - } } Object oldValue = argSpec.getValue(); String traceMessage = initValueMessage; @@ -13071,6 +13138,9 @@ public class CommandLine { } initialized.add(argSpec); + if (argSpec.typeInfo().isOptional()) { + newValue = getOptionalOfNullable(newValue); + } if (tracer.isInfo()) { tracer.info(traceMessage, argSpec.toString(), String.valueOf(oldValue), String.valueOf(newValue), argDescription, argSpec.scopeString()); } int pos = getPosition(argSpec); argSpec.setValue(newValue); @@ -13173,7 +13243,7 @@ public class CommandLine { for (String value : values) { String[] keyValue = splitKeyValue(argSpec, value); Object mapKey = tryConvert(argSpec, index, keyConverter, keyValue[0], 0); - String rawMapValue = keyValue.length == 1 ? ((OptionSpec) argSpec).fallbackValue() : keyValue[1]; + String rawMapValue = keyValue.length == 1 ? argSpec.mapFallbackValue() : keyValue[1]; Object mapValue = tryConvert(argSpec, index, valueConverter, rawMapValue, 1); result.put(mapKey, mapValue); if (tracer.isInfo()) { tracer.info("Putting [%s : %s] in %s<%s, %s> %s for %s on %s%n", String.valueOf(mapKey), String.valueOf(mapValue), @@ -13199,7 +13269,7 @@ public class CommandLine { for (String value : values) { String[] keyValue = splitKeyValue(argSpec, value); tryConvert(argSpec, -1, keyConverter, keyValue[0], 0); - String mapValue = keyValue.length == 1 ? "" : keyValue[1]; + String mapValue = keyValue.length == 1 ? argSpec.mapFallbackValue() : keyValue[1]; tryConvert(argSpec, -1, valueConverter, mapValue, 1); } return true; @@ -13212,17 +13282,18 @@ public class CommandLine { private String[] splitKeyValue(ArgSpec argSpec, String value) { String[] keyValue = ArgSpec.splitRespectingQuotedStrings(value, 2, config(), argSpec, "="); - // validation disabled for #1214: support for -Dkey map options - //if (keyValue.length < 2) { - // String splitRegex = argSpec.splitRegex(); - // if (splitRegex.length() == 0) { - // throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", - // argSpec, 0) + " should be in KEY=VALUE format but was " + value, argSpec, value); - // } else { - // throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", - // argSpec, 0) + " should be in KEY=VALUE[" + splitRegex + "KEY=VALUE]... format but was " + value, argSpec, value); - // } - //} + // #1214: support for -Dkey map options + // validation is disabled if `mapFallbackValue` is specified + if (keyValue.length < 2 && ArgSpec.UNSPECIFIED.equals(argSpec.mapFallbackValue())) { + String splitRegex = argSpec.splitRegex(); + if (splitRegex.length() == 0) { + throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", + argSpec, 0) + " should be in KEY=VALUE format but was " + value, argSpec, value); + } else { + throw new ParameterException(CommandLine.this, "Value for option " + optionDescription("", + argSpec, 0) + " should be in KEY=VALUE[" + splitRegex + "KEY=VALUE]... format but was " + value, argSpec, value); + } + } return keyValue; } diff --git a/src/test/java/picocli/CommandLineTest.java b/src/test/java/picocli/CommandLineTest.java index e85cbb6d..9efe130f 100644 --- a/src/test/java/picocli/CommandLineTest.java +++ b/src/test/java/picocli/CommandLineTest.java @@ -3064,17 +3064,12 @@ public class CommandLineTest { assertEquals("-Dspring.profiles.active=test -Dspring.mail.host=smtp.mailtrap.io", c.parameters.get("AppOptions")); args = new String[] {"-p", "\"AppOptions=-Dspring.profiles.active=test -Dspring.mail.host=smtp.mailtrap.io\""}; -// try { -// c = CommandLine.populateCommand(new MyCommand(), args); -// fail("Expected exception"); // superceded by #1214 -// } catch (ParameterException ex) { -// assertEquals("Value for option option '--parameter' () should be in KEY=VALUE format but was \"AppOptions=-Dspring.profiles.active=test -Dspring.mail.host=smtp.mailtrap.io\"", ex.getMessage()); -// } - c = CommandLine.populateCommand(new MyCommand(), args); - assertEquals(1, c.parameters.size()); - String key = "\"AppOptions=-Dspring.profiles.active=test -Dspring.mail.host=smtp.mailtrap.io\""; - assertEquals(new HashSet(Collections.singletonList(key)), c.parameters.keySet()); - assertEquals("", c.parameters.get(key)); + try { + c = CommandLine.populateCommand(new MyCommand(), args); + fail("Expected exception"); // superceded by #1214 + } catch (ParameterException ex) { + assertEquals("Value for option option '--parameter' () should be in KEY=VALUE format but was \"AppOptions=-Dspring.profiles.active=test -Dspring.mail.host=smtp.mailtrap.io\"", ex.getMessage()); + } c = new MyCommand(); new CommandLine(c).setTrimQuotes(true).parseArgs(args); diff --git a/src/test/java/picocli/MapOptionsTest.java b/src/test/java/picocli/MapOptionsTest.java index f46b6f4d..587a45d5 100644 --- a/src/test/java/picocli/MapOptionsTest.java +++ b/src/test/java/picocli/MapOptionsTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.RestoreSystemProperties; import org.junit.rules.TestRule; +import picocli.CommandLine.Model.ArgSpec; import picocli.CommandLine.Option; import picocli.CommandLine.ParameterException; @@ -29,19 +30,55 @@ public class MapOptionsTest { public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false"); @Test - public void testEmptyStringIfNoValue() { + public void testErrorIfNoMapFallbackValue() { class App { @Option(names = "-D") Map map; } + try { + CommandLine.populateCommand(new App(), "-Dkey"); + fail("Expected exception"); + } catch (ParameterException ex) { + assertEquals("Value for option option '-D' () should be in KEY=VALUE format but was key", ex.getMessage()); + } + } + + @Test + public void testErrorIfMapFallbackValueIsUnspecified() { + class App { + @Option(names = "-D", mapFallbackValue = "__unspecified__") Map map; + } + try { + CommandLine.populateCommand(new App(), "-Dkey"); + fail("Expected exception"); + } catch (ParameterException ex) { + assertEquals("Value for option option '-D' () should be in KEY=VALUE format but was key", ex.getMessage()); + } + } + + @Test + public void testMapFallbackValueEmptyString() { + class App { + @Option(names = "-D", mapFallbackValue = "") Map map; + } App app = CommandLine.populateCommand(new App(), "-Dkey"); assertEquals(1, app.map.size()); assertEquals("", app.map.get("key")); } @Test - public void testEmptyStringIfNoValueMultiple() { + public void testMapFallbackValueNull() { class App { - @Option(names = "-D") Map map; + @Option(names = "-D", mapFallbackValue = ArgSpec.NULL_VALUE) Map map; + } + App app = CommandLine.populateCommand(new App(), "-Dkey"); + assertEquals(1, app.map.size()); + assertEquals(null, app.map.get("key")); + } + + @Test + public void testMapFallbackValueEmptyStringMultiple() { + class App { + @Option(names = "-D", mapFallbackValue = "") Map map; } App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2", "-Dkey3"); assertEquals(3, app.map.size()); @@ -51,9 +88,21 @@ public class MapOptionsTest { } @Test - public void testTypeConversionErrorIfNoValue() { + public void testMapFallbackValueNullMultiple() { class App { - @Option(names = "-D") Map map; + @Option(names = "-D", mapFallbackValue = ArgSpec.NULL_VALUE) Map map; + } + App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2", "-Dkey3"); + assertEquals(3, app.map.size()); + assertEquals(null, app.map.get("key1")); + assertEquals(null, app.map.get("key2")); + assertEquals(null, app.map.get("key3")); + } + + @Test + public void testTypeConversionErrorIfValueCannotBeConverted() { + class App { + @Option(names = "-D", mapFallbackValue = "") Map map; } try { CommandLine.populateCommand(new App(), "-Dkey"); diff --git a/src/test/java/picocli/ModelArgGroupSpecTest.java b/src/test/java/picocli/ModelArgGroupSpecTest.java index b592caca..33ecc3d5 100644 --- a/src/test/java/picocli/ModelArgGroupSpecTest.java +++ b/src/test/java/picocli/ModelArgGroupSpecTest.java @@ -38,6 +38,7 @@ public class ModelArgGroupSpecTest { public boolean isCollection() { return false; } public boolean isMap() { return false; } public boolean isEnum() { return false; } + public boolean isOptional() { return false; } public List getEnumConstantNames() { return null; } public String getClassName() { return null; } public String getClassSimpleName() { return null; } diff --git a/src/test/java/picocli/ModelArgSpecTest.java b/src/test/java/picocli/ModelArgSpecTest.java index 436f37e1..7024485b 100644 --- a/src/test/java/picocli/ModelArgSpecTest.java +++ b/src/test/java/picocli/ModelArgSpecTest.java @@ -257,6 +257,7 @@ public class ModelArgSpecTest { public boolean isMultiValue() { return false; } public boolean isArray() { return false; } public boolean isCollection() { return false; } + public boolean isOptional() { return false; } public boolean isEnum() { return false; } public List getEnumConstantNames() { return null; } public String getClassName() { return null; } diff --git a/src/test/java/picocli/ModelTypedMemberTest.java b/src/test/java/picocli/ModelTypedMemberTest.java index bd770510..1c587057 100644 --- a/src/test/java/picocli/ModelTypedMemberTest.java +++ b/src/test/java/picocli/ModelTypedMemberTest.java @@ -27,7 +27,13 @@ public class ModelTypedMemberTest { @CommandLine.Parameters List[]>> list; } - assertEquals("", CommandLine.Model.CommandSpec.forAnnotatedObject(new App()).positionalParameters().get(0).paramLabel()); + try { + new CommandLine(new App()); + fail("Expected exception"); + } catch (CommandLine.InitializationException ex) { + String msg = "Unsupported generic type java.util.List[]>>. Only List, Map, Optional, and Map> are supported. Type parameters may be char[], a non-array type, or a wildcard type with an upper or lower bound."; + assertEquals(msg, ex.getMessage()); + } } @Test diff --git a/src/test/java/picocli/TypeConversionTest.java b/src/test/java/picocli/TypeConversionTest.java index b50261a4..a86e84e8 100644 --- a/src/test/java/picocli/TypeConversionTest.java +++ b/src/test/java/picocli/TypeConversionTest.java @@ -751,9 +751,9 @@ public class TypeConversionTest { commandLine.execute("anything"); //System.out.println(sw); assertTrue(sw.toString().startsWith("picocli.CommandLine$ParameterException: Invalid value for positional parameter at index 0 (): I am always thrown")); - assertTrue(sw.toString().contains(String.format("Caused by: picocli.CommandLine$TypeConversionException: I am always thrown%n" + - "\tat picocli.TypeConversionTest$TypeConversionExceptionConverter.convert(TypeConversionTest.java:722)%n" + - "\tat picocli.TypeConversionTest$TypeConversionExceptionConverter.convert(TypeConversionTest.java:720)%n"))); + assertTrue(sw.toString(), sw.toString().contains(String.format("Caused by: picocli.CommandLine$TypeConversionException: I am always thrown%n" + + "\tat picocli.TypeConversionTest$TypeConversionExceptionConverter.convert(TypeConversionTest.java:724)%n" + + "\tat picocli.TypeConversionTest$TypeConversionExceptionConverter.convert(TypeConversionTest.java:722)%n"))); } static class CustomConverter implements ITypeConverter { public Integer convert(String value) { return Integer.parseInt(value); }