mirror of
https://github.com/jlengrand/picocli.git
synced 2026-03-10 08:41:17 +00:00
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user