Skip to content

Commit

Permalink
[#280] API: fallbackValue for options with optional parameter: assi…
Browse files Browse the repository at this point in the history
…gn this value when the option was specified on the command line without parameter.
  • Loading branch information
remkop committed Jun 11, 2019
1 parent 2a0bd5c commit ba48edf
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 24 deletions.
27 changes: 22 additions & 5 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# picocli Release Notes


# <a name="4.0.0-rc-1"></a> Picocli 4.0.0-rc-1
# <a name="4.0.0-rc-1"></a> Picocli 4.0.0-rc-1 (UNRELEASED)
The picocli community is pleased to announce picocli 4.0.0-rc-1.

Bugfixes and improvements.

This release introduces a new attribute on the `Option` annotation: `fallbackValue` for options with optional parameter: assign this value when the option was specified on the command line without parameter.

_Please try this and provide feedback. We can still make changes._

_What do you think of the `@ArgGroup` annotations API? What about the programmatic API? Does it work as expected? Are the input validation error messages correct and clear? Is the documentation clear and complete? Anything you want to change or improve? Any other feedback?_
Expand All @@ -24,11 +26,26 @@ Picocli follows [semantic versioning](http://semver.org/).

## <a name="4.0.0-rc-1-new"></a> New and Noteworthy

### `fallbackValue` API
This release introduces a new attribute on the `Option` annotation: `fallbackValue` for options with optional parameter: assign this value when the option was specified on the command line without parameter.

This is different from the `defaultValue`, which is assigned if the option is not specified at all on the command line.

Using a `fallbackValue` allows applications to distinguish between cases where
* the option was not specified on the command line (default value assigned)
* the option was specified without parameter on the command line (fallback value assigned)
* the option was specified with parameter on the command line (command line argument value assigned)

This is useful to define options that can function as a boolean "switch" and optionally allow users to provide a (strongly typed) extra parameter value.

The option description may contain the `${FALLBACK-VALUE}` variable which will be replaced with the actual fallback value when the usage help is shown.

## <a name="4.0.0-rc-1-fixes"></a> Fixed issues
- [#719] Bugfix: options with variable arity should stop consuming arguments on custom end-of-options delimiter
- [#720] `@Unmatched` list should be cleared prior to subsequent invocations
- [#721] Add public method Text.getCJKAdjustedLength()
- [#717] Negatable options change: avoid unmappable character `±` for synopsis: it renders as scrambled characters in encoding ASCII and in some terminals
- [#280] API: `fallbackValue` for options with optional parameter: assign this value when the option was specified on the command line without parameter. Thanks to [Paolo Di Tommaso](https://github.com/pditommaso) and [marinier](https://github.com/marinier) for the suggestion and in-depth discussion.
- [#721] API: Add public method Text.getCJKAdjustedLength().
- [#717] Negatable options change: avoid unmappable character `±` for synopsis: it renders as scrambled characters in encoding ASCII and in some terminals.
- [#719] Bugfix: options with variable arity should stop consuming arguments on custom end-of-options delimiter.
- [#720] Bugfix: `@Unmatched` list should be cleared prior to subsequent invocations.
- [#723] Bugfix: variables in `defaultValue` were not expanded in usage help option description line for `showDefaultValues = true`. Thanks to [Mikaël Barbero](https://github.com/mbarbero) for raising this.
- [#722] Bugfix: synopsis of deeply nested `@ArgGroup` shows `@Options` duplicate on outer level of command. Thanks to [Shane Rowatt](https://github.com/srowatt) for raising this.

Expand Down
41 changes: 29 additions & 12 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ For annotated fields, it is simplest to declare the field with a value:
int count = 123; // default value is 123
----

[#defaultValue-annotation]
=== `defaultValue` Annotation
For <<option-parameters-methods,@Option and @Parameters-annotated methods>> and <<command-methods,@Command-annotated methods>>, use the `defaultValue` annotation attribute. For example, for an annotated interface:
[source,java]
Expand Down Expand Up @@ -758,7 +759,7 @@ class CommandMethod {
}
----

Note that you can use the `${DEFAULT-VALUE}` variable in the `description` of the option or positional parameter and picocli will <<Show Default Values,show>> the actual default value.
Note that you can use the `${DEFAULT-VALUE}` <<Predefined Variables,variable>> in the `description` of the option or positional parameter and picocli will <<Show Default Values,show>> the actual default value.

=== Default Provider
Finally, you can specify a default provider in the `@Command` annotation:
Expand Down Expand Up @@ -791,6 +792,15 @@ public interface IDefaultValueProvider {
}
----

[#fallbackValue-annotation]
=== `fallbackValue` Annotation

For options with an optional parameter, the fallback value (introduced in picocli 4.0) is assigned to the annotated element
if the option is specified on the command line without an option parameter.

This is different from the `defaultValue`, which is assigned if the option is not specified at all on the command line.

See <<Optional Values>> for details.

== Multiple Values
Multi-valued options and positional parameters are annotated fields that can capture multiple values from the command line.
Expand Down Expand Up @@ -933,16 +943,21 @@ When a `@Parameters` field is applied (because its index matches the index of th

=== Optional Values
If an option is defined with `arity = "0..1"`, it may or not have a parameter value.
If such an option is specified without a value on the command line, it is assigned an empty String (starting from picocli 2.3).
If the option is not specified, it keeps its default value. For example:
If such an option is specified without a value on the command line, it is assigned the <<fallbackValue-annotation,fallback value>>.
The `fallbackValue` annotation was introduced in picocli 4.0; prior to this, (from picocli 2.3) an empty String was assigned.

If the option is not specified on the command line at all, it keeps its default value. For example:

[source, java]
----
class OptionalValueDemo implements Runnable {
@Option(names = "-x", arity = "0..1", description = "optional parameter")
String x;
@Option(names = "-x", arity = "0..1",
defaultValue = "-1", fallbackValue = "-2",
description = "Optional parameter. Default: ${DEFAULT-VALUE}, " +
"if specified without parameter: ${FALLBACK-VALUE}")
int x;
public void run() { System.out.printf("x = '%s'%n", x); }
public void run() { System.out.printf("x = %s%n", x); }
public static void main(String... args) {
CommandLine.run(new OptionalValueDemo(), args);
Expand All @@ -952,18 +967,19 @@ class OptionalValueDemo implements Runnable {
Gives the following results:
[source, bash]
----
java OptionalValueDemo -x value
x = 'value'
java OptionalValueDemo -x 100
x = 100
java OptionalValueDemo -x
x = ''
x = -2
java OptionalValueDemo
x = 'null'
x = -1
----
From picocli 3.0, options with non-String types can specify a <<Custom Type Converters,type converter>> to convert the empty String to a strongly typed value when the option is specified without a value.

Any String value is converted to the type of the annotated element before it is assigned to the element. Options and positional parameters may define a <<Custom Type Converters,custom type converter>> if necessary.

Note that the option description may contain the `${FALLBACK-VALUE}` <<Predefined Variables,variable>> which will be replaced with the actual fallback value when the usage help is shown.

== Required Arguments
=== Required Options
Expand Down Expand Up @@ -3402,7 +3418,8 @@ The following variables are predefined:
[grid=cols,cols=".<3,.^1,.<3,.<3",options="header"]
|===
|Variable | Since | Use in | Meaning
|`${DEFAULT-VALUE}`| 3.2 | the description for an option or positional parameter|replaced with the default value for that option or positional parameter
|`${DEFAULT-VALUE}`| 3.2 | the description for an option or positional parameter|replaced with the <<defaultValue-annotation,default value>> for that option or positional parameter
|`${FALLBACK-VALUE}`| 4.0 | the description for an option with optional parameter|replaced with the <<fallbackValue-annotation,fallback value>> for that option or positional parameter
|`${COMPLETION-CANDIDATES}`| 3.2 | the description for an option or positional parameter| replaced with the completion candidates for that option or positional parameter
|`${COMMAND-NAME}`| 4.0 | any section of the usage help message for a command|replaced with the name of the command
|`${COMMAND-FULL-NAME}`| 4.0 | any section of the usage help message for a command| replaced with the fully qualified name of the command (that is, preceded by its parent fully qualified name)
Expand Down
62 changes: 58 additions & 4 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -3425,7 +3425,8 @@ private static class NoCompletionCandidates implements Iterable<String> {
boolean hidden() default false;

/** Returns the default value of this option, before splitting and type conversion.
* @return a String that (after type conversion) will be used as the value for this option if no value was specified on the command line
* @return a String that (after type conversion) will be used as the value for this option if the option was not specified on the command line
* @see #fallbackValue()
* @since 3.2 */
String defaultValue() default "__no_default_value__";

Expand Down Expand Up @@ -3499,6 +3500,25 @@ private static class NoCompletionCandidates implements Iterable<String> {
* @see CommandLine#setNegatableOptionTransformer(INegatableOptionTransformer)
* @since 4.0 */
boolean negatable() default false;

/**
* For options with an optional parameter (for example, {@code arity = "0..1"}), this value is assigned to the annotated element
* if the option is specified on the command line without an option parameter.
* <p>
* This is different from the {@link #defaultValue()}, which is assigned if the option is not specified at all on the command line.
* </p><p>
* Using a {@code fallbackValue} allows applications to distinguish between</p>
* <ul>
* <li>option was not specified on the command line (default value assigned)</li>
* <li>option was specified without parameter on the command line (fallback value assigned)</li>
* <li>option was specified with parameter on the command line (command line argument value assigned)</li>
* </ul>
* <p>This is useful to define options that can function as a boolean "switch"
* and optionally allow users to provide a (strongly typed) extra parameter value.
* </p>
* @see OptionSpec#fallbackValue()
* @since 4.0 */
String fallbackValue() default "";
}
/**
* <p>
Expand Down Expand Up @@ -6595,6 +6615,7 @@ void initFrom(ParserSpec settings) {
* @since 3.0 */
public abstract static class ArgSpec {
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__";

Expand Down Expand Up @@ -6761,9 +6782,11 @@ private String[] expandVariables(String[] desc) {
}
}
String defaultValueString = defaultValueString(false); // interpolate later
String fallbackValueString = isOption() ? ((OptionSpec) this).fallbackValue : ""; // interpolate later
String[] result = new String[desc.length];
for (int i = 0; i < desc.length; i++) {
result[i] = format(desc[i].replace(DESCRIPTION_VARIABLE_DEFAULT_VALUE, defaultValueString.replace("%", "%%"))
.replace(DESCRIPTION_VARIABLE_FALLBACK_VALUE, fallbackValueString.replace("%", "%%"))
.replace(DESCRIPTION_VARIABLE_COMPLETION_CANDIDATES, candidates.toString()));
}
return interpolate(result);
Expand Down Expand Up @@ -6816,14 +6839,15 @@ private String[] expandVariables(String[] desc) {
* @since 4.0 */
public Object userObject() { return userObject; }

/** Returns the default value of this option or positional parameter, before splitting and type conversion.
/** 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
* the default value from the default value provider, and this method is only called if the default provider is
* {@code null} or returned a {@code null} value.
* @return the programmatically set default value of this option/positional parameter,
* returning {@code null} means this option or positional parameter does not have a default
* @see CommandSpec#defaultValueProvider()
* @see OptionSpec#fallbackValue()
*/
public String defaultValue() { return interpolate(defaultValue); }
/** Returns the initial value this option or positional parameter. If {@link #hasInitialValue()} is true,
Expand Down Expand Up @@ -7465,6 +7489,7 @@ public static class OptionSpec extends ArgSpec implements IOrdered {
private boolean usageHelp;
private boolean versionHelp;
private boolean negatable;
private String fallbackValue;
private int order;

public static OptionSpec.Builder builder(String name, String... names) {
Expand All @@ -7488,6 +7513,7 @@ private OptionSpec(Builder builder) {
versionHelp = builder.versionHelp;
order = builder.order;
negatable = builder.negatable;
fallbackValue = builder.fallbackValue;

if (names.length == 0 || Arrays.asList(names).contains("")) {
throw new InitializationException("Invalid names: " + Arrays.toString(names));
Expand Down Expand Up @@ -7554,6 +7580,14 @@ protected boolean internalShowDefaultValue(boolean usageMessageShowDefaults) {
* @see Option#negatable()
* @since 4.0 */
public boolean negatable() { return negatable; }

/** 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.
* @see Option#fallbackValue()
* @see #defaultValue()
* @since 4.0 */
public String fallbackValue() { return interpolate(fallbackValue); }

public boolean equals(Object obj) {
if (obj == this) { return true; }
if (!(obj instanceof OptionSpec)) { return false; }
Expand All @@ -7563,6 +7597,8 @@ public boolean equals(Object obj) {
&& usageHelp == other.usageHelp
&& versionHelp == other.versionHelp
&& order == other.order
&& negatable == other.negatable
&& Assert.equals(fallbackValue, other.fallbackValue)
&& new HashSet<String>(Arrays.asList(names)).equals(new HashSet<String>(Arrays.asList(other.names)));
return result;
}
Expand All @@ -7572,6 +7608,8 @@ public int hashCode() {
+ 37 * Assert.hashCode(usageHelp)
+ 37 * Assert.hashCode(versionHelp)
+ 37 * Arrays.hashCode(names)
+ 37 * Assert.hashCode(negatable)
+ 37 * Assert.hashCode(fallbackValue)
+ 37 * order;
}

Expand All @@ -7584,6 +7622,7 @@ public static class Builder extends ArgSpec.Builder<Builder> {
private boolean usageHelp;
private boolean versionHelp;
private boolean negatable;
private String fallbackValue = "";
private int order = DEFAULT_ORDER;

private Builder(String[] names) { this.names = names; }
Expand All @@ -7594,6 +7633,7 @@ private Builder(OptionSpec original) {
usageHelp = original.usageHelp;
versionHelp = original.versionHelp;
negatable = original.negatable;
fallbackValue = original.fallbackValue;
order = original.order;
}
private Builder(IAnnotatedElement member, IFactory factory) {
Expand All @@ -7604,6 +7644,7 @@ private Builder(IAnnotatedElement member, IFactory factory) {
usageHelp = option.usageHelp();
versionHelp = option.versionHelp();
negatable = option.negatable();
fallbackValue = option.fallbackValue();
order = option.order();
}

Expand Down Expand Up @@ -7635,6 +7676,12 @@ private Builder(IAnnotatedElement member, IFactory factory) {
* @since 4.0 */
public boolean negatable() { return negatable; }

/** 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.
* @see Option#fallbackValue()
* @since 4.0 */
public String fallbackValue() { return fallbackValue; }

/** Returns the position in the options list in the usage help message at which this option should be shown.
* Options with a lower number are shown before options with a higher number.
* This attribute is only honored if {@link UsageMessageSpec#sortOptions()} is {@code false} for this command.
Expand All @@ -7658,6 +7705,12 @@ private Builder(IAnnotatedElement member, IFactory factory) {
* @since 4.0 */
public Builder negatable(boolean negatable) { this.negatable = negatable; return self(); }

/** 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.
* @see Option#fallbackValue()
* @since 4.0 */
public Builder fallbackValue(String fallbackValue) { this.fallbackValue = fallbackValue; return self(); }

/** Sets the position in the options list in the usage help message at which this option should be shown, and returns this builder.
* @since 3.9 */
public Builder order(int order) { this.order = order; return self(); }
Expand Down Expand Up @@ -10903,12 +10956,13 @@ private int applyValueToSingleValuedField(ArgSpec argSpec,
consumed = 0;
}
} else { // non-boolean option with optional value #325, #279
String fallbackValue = argSpec.isOption() ? ((OptionSpec) argSpec).fallbackValue() : "";
if (isOption(value)) { // value is not a parameter
actualValue = "";
actualValue = fallbackValue;
optionalValueExists = false;
consumed = 0;
} else if (value == null) { // stack is empty, option with arity=0..1 was the last arg
actualValue = "";
actualValue = fallbackValue;
optionalValueExists = false;
consumed = 0;
}
Expand Down
Loading

0 comments on commit ba48edf

Please sign in to comment.