[#1241][#1108][#1214] mapFallbackValue API; key-only Map args; Optional<T> support

This commit is contained in:
Remko Popma
2020-10-30 21:24:45 +09:00
parent 7fe0d4ce37
commit d77e676075
12 changed files with 199 additions and 49 deletions

View File

@@ -23,9 +23,10 @@ Picocli follows [semantic versioning](http://semver.org/).
## <a name="4.6.0-new"></a> New and Noteworthy
## <a name="4.6.0-fixes"></a> 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<T>` 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.
## <a name="4.6.0-breaking-changes"></a> Potential breaking changes
This release has no breaking changes.
Added method `isOptional()` to the `picocli.CommandLine.Model.ITypeInfo` interface.
# <a name="4.5.2"></a> Picocli 4.5.2

View File

@@ -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();

View File

@@ -32,7 +32,7 @@ public class MapOptionsOptionalTest {
@Test
public void testOptionalIfNoValue() {
class App {
@Option(names = "-D") Map<String, Optional<String>> map;
@Option(names = "-D", mapFallbackValue = "") Map<String, Optional<String>> 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<String, Optional<String>> map;
@Option(names = "-D", mapFallbackValue = "_NULL_") Map<String, Optional<String>> 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<String, Optional<String>> map;
@Option(names = "-D", mapFallbackValue = "") Map<String, Optional<String>> 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<String, Optional<String>> map;
@Option(names = "-D", mapFallbackValue = "_NULL_") Map<String, Optional<String>> 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<String, Boolean> map;
@Option(names = "-E", mapFallbackValue = "true") Map<String, Boolean> 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<String, Optional<Integer>> map;
@Option(names = "-D", mapFallbackValue = "_NULL_") Map<String, Optional<Integer>> map;
}
App app = CommandLine.populateCommand(new App(), "-Dkey1", "-Dkey2");
assertEquals(2, app.map.size());

View File

@@ -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<String> 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(),

View File

@@ -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;

View File

@@ -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}.
* <p>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}.</p>
* <p>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.</p>
* @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<? extends IParameterConsumer> 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}.
* <p>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}.</p>
* <p>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.</p>
* @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<String> 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}.
* <p>If the special value {@link #NULL_VALUE} is set on the builder, the {@code ArgSpec.mapFallbackValue()} getter returns {@code null}.</p>
* <p>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.</p>
* @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}.
* <p>If the special value {@link #NULL_VALUE} is set on the builder, the {@code ArgSpec.mapFallbackValue()} getter returns {@code null}.</p>
* <p>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.</p>
* @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}.
* <p>Setting the special value {@link #NULL_VALUE}, will cause the getter method to return {@code null}.</p>
* <p>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.</p>
* @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.
* <p>If the special value {@link #NULL_VALUE} is set, this method returns {@code null}.</p>
* @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.
* <p>If the special value {@link #NULL_VALUE} is set on the builder, the {@code OptionSpec.fallbackValue()} getter returns {@code null}.</p>
* @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.
* <p>Setting the special value {@link #NULL_VALUE}, will cause the getter method to return {@code null}.</p>
* @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<ArgSpec>(), 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;
}

View File

@@ -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' (<String=String>) 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<String>(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' (<String=String>) 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);

View File

@@ -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<String, String> map;
}
try {
CommandLine.populateCommand(new App(), "-Dkey");
fail("Expected exception");
} catch (ParameterException ex) {
assertEquals("Value for option option '-D' (<String=String>) should be in KEY=VALUE format but was key", ex.getMessage());
}
}
@Test
public void testErrorIfMapFallbackValueIsUnspecified() {
class App {
@Option(names = "-D", mapFallbackValue = "__unspecified__") Map<String, String> map;
}
try {
CommandLine.populateCommand(new App(), "-Dkey");
fail("Expected exception");
} catch (ParameterException ex) {
assertEquals("Value for option option '-D' (<String=String>) should be in KEY=VALUE format but was key", ex.getMessage());
}
}
@Test
public void testMapFallbackValueEmptyString() {
class App {
@Option(names = "-D", mapFallbackValue = "") Map<String, String> 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<String, String> map;
@Option(names = "-D", mapFallbackValue = ArgSpec.NULL_VALUE) Map<String, String> 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<String, String> 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<String, Integer> map;
@Option(names = "-D", mapFallbackValue = ArgSpec.NULL_VALUE) Map<String, String> 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<String, Integer> map;
}
try {
CommandLine.populateCommand(new App(), "-Dkey");

View File

@@ -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<String> getEnumConstantNames() { return null; }
public String getClassName() { return null; }
public String getClassSimpleName() { return null; }

View File

@@ -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<String> getEnumConstantNames() { return null; }
public String getClassName() { return null; }

View File

@@ -27,7 +27,13 @@ public class ModelTypedMemberTest {
@CommandLine.Parameters
List<Class<? extends Class<? extends String>[]>> list;
}
assertEquals("<list>", 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<java.lang.Class<? extends java.lang.Class<? extends java.lang.String>[]>>. Only List<T>, Map<K,V>, Optional<T>, and Map<K, Optional<V>> 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

View File

@@ -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 (<sqlTypeParam>): 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<Integer> {
public Integer convert(String value) { return Integer.parseInt(value); }