From 57f76cc8ccb05602d3d1707ec0386f9f95494d24 Mon Sep 17 00:00:00 2001
From: Remko Popma
Date: Mon, 6 May 2019 10:15:48 +0900
Subject: [PATCH] [#682][#680] annotation API for exitCodeList and
exitCodeListHeading; bugfix in interpolator
---
RELEASE-NOTES.md | 2 +
docs/index.adoc | 65 ++++----
src/main/java/picocli/AutoComplete.java | 18 +-
src/main/java/picocli/CommandLine.java | 87 +++++-----
src/test/java/picocli/ExecuteTest.java | 154 ++++++++++++------
src/test/java/picocli/I18nSuperclass.java | 5 +-
src/test/java/picocli/I18nTest.java | 13 ++
src/test/java/picocli/InterpolatorTest.java | 20 +++
.../I18nSuperclass_Messages.properties | 4 +
9 files changed, 234 insertions(+), 134 deletions(-)
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 21f96c999..e79bf6665 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -162,8 +162,10 @@ With the new execute API the ColorScheme class will start to play a more central
- [#663] How to remove stacktraces on error. Thanks to [Nicolas Mingo](https://github.com/nicolasmingo) and [jrevault](https://github.com/jrevault) for raising this and subsequent discussion.
- [#672] Need way to send errors back from subcommand. Thanks to [Garret Wilson](https://github.com/garretwilson) for raising this.
- [#678] Exit Status section in usage help message.
+- [#680] Add annotation API for exitCodeList and exitCodeListHeading.
- [#575] Use mixinStandardHelpOptions in `AutoComplete$App` (add the `--version` option)
- [#676] Bugfix: non-defined variables in `defaultValue` now correctly resolve to `null`, and options and positional parameters are now correctly considered `required` only if their default value is `null` after variable interpolation. Thanks to [ifedorenko](https://github.com/ifedorenko) for raising this.
+- [#682] Bug: incorrect evaluation for multiple occurrences of a variable.
- [#679] Documentation: Update examples for new execute API. Add examples for exit code control and custom exception handlers.
- [#681] Documentation: Add exit code section to Internationalization example in user manual.
diff --git a/docs/index.adoc b/docs/index.adoc
index dc0c32065..ae26c831b 100644
--- a/docs/index.adoc
+++ b/docs/index.adoc
@@ -1330,35 +1330,12 @@ When the end user specified invalid input, the `execute` method prints an error
If the business logic of the command throws an exception, the `execute` method prints the stack trace of the exception and returns an exit code. This can be customized by configuring a `IExecutionExceptionHandler`.
=== Usage Help Exit Code Section
-By default, the usage help message does not display the exit code section.
-Applications that call `System.exit` need to configure this manually by calling `CommandLine.setExitCodeHelpSection(String, Map)`
-or by calling `UsageMessageSpec.exitCodeListHeading` and `UsageMessageSpec.exitCodeList`. For example:
+By default, the usage help message does not include exit code information.
+Applications that call `System.exit` need to configure the usage help message to show exit code details,
+either with the `exitCodeListHeading` and `exitCodeList` annotation attributes,
+or programmatically by calling `UsageMessageSpec.exitCodeListHeading` and `UsageMessageSpec.exitCodeList`.
-```java
-// import static picocli.CommandLine.Model.UsageMessageSpec.keyValuesMap;
-@Command class App {}
-CommandLine cmd = new CommandLine(new App());
-cmd.setExitCodeHelpSection("Exit Codes:%n",
- keyValuesMap(" 0:Successful program execution",
- "64:Usage error: user input for the command was incorrect, " +
- "e.g., the wrong number of arguments, a bad flag, " +
- "a bad syntax in a parameter, etc.",
- "70:Internal software error: an exception occurred when invoking " +
- "the business logic of this command."));
-cmd.usage(System.out);
-```
-
-This will print the following message to the console:
-
-```
-Usage:
-Exit Codes:
- 0 Successful program execution
- 64 Usage error: user input for the command was incorrect, e.g., the wrong
- number of arguments, a bad flag, a bad syntax in a parameter, etc.
- 70 Internal software error: an exception occurred when invoking the
- business logic of this command.
-```
+See <> for details.
=== Execution Configuration
@@ -2103,6 +2080,38 @@ Use the `footer` attribute to specify one or more lines to show below the genera
Each element of the attribute String array is displayed on a separate line.
+
+=== Exit Code List
+By default, the usage help message does not display <> information.
+Applications that call `System.exit` need to configure the `exitCodeListHeading` and `exitCodeList` annotation attributes.
+For example:
+
+```java
+@Command(mixinStandardHelpOptions = true,
+ exitCodeListHeading = "Exit Codes:%n",
+ exitCodeList = {
+ " 0:Successful program execution",
+ "64:Usage error: user input for the command was incorrect, " +
+ "e.g., the wrong number of arguments, a bad flag, " +
+ "a bad syntax in a parameter, etc.",
+ "70:Internal software error: an exception occurred when invoking " +
+ "the business logic of this command."})
+class App {}
+new CommandLine(new App()).usage(System.out);
+```
+
+This will print the following usage help message to the console:
+
+```
+Usage:
+Exit Codes:
+ 0 Successful program execution
+ 64 Usage error: user input for the command was incorrect, e.g., the wrong
+ number of arguments, a bad flag, a bad syntax in a parameter, etc.
+ 70 Internal software error: an exception occurred when invoking the
+ business logic of this command.
+```
+
=== Format Specifiers
All usage help message elements can have embedded line separator (`%n`) format specifiers.
These are converted to the platform-specific line separator when the usage help message is printed.
diff --git a/src/main/java/picocli/AutoComplete.java b/src/main/java/picocli/AutoComplete.java
index 190701839..70da9615b 100644
--- a/src/main/java/picocli/AutoComplete.java
+++ b/src/main/java/picocli/AutoComplete.java
@@ -71,14 +71,6 @@ public int handleExecutionException(Exception ex, CommandLine commandLine, Parse
};
int exitCode = new CommandLine(new App())
.setExecutionExceptionHandler(errorHandler)
- .setExitCodeHelpSection("%nExit Codes:%n",
- keyValuesMap("0:Successful program execution",
- "1:Usage error: user input for the command was incorrect, " +
- "e.g., the wrong number of arguments, a bad flag, " +
- "a bad syntax in a parameter, etc.",
- "2:The specified command script exists (Specify --force to overwrite).",
- "3:The specified completion script exists (Specify --force to overwrite).",
- "4:An exception occurred while generating the completion script."))
.execute(args);
if ((exitCode == EXIT_CODE_SUCCESS && exitOnSuccess()) || (exitCode != EXIT_CODE_SUCCESS && exitOnError())) {
System.exit(exitCode);
@@ -112,6 +104,16 @@ private static boolean syspropDefinedAndNotFalse(String key) {
" when an error occurs",
"If these system properties are not defined or have value \"false\", this program completes without terminating the JVM."
},
+ exitCodeListHeading = "%nExit Codes:%n",
+ exitCodeList = {
+ "0:Successful program execution",
+ "1:Usage error: user input for the command was incorrect, " +
+ "e.g., the wrong number of arguments, a bad flag, " +
+ "a bad syntax in a parameter, etc.",
+ "2:The specified command script exists (Specify --force to overwrite).",
+ "3:The specified completion script exists (Specify --force to overwrite).",
+ "4:An exception occurred while generating the completion script."
+ },
exitCodeOnInvalidInput = EXIT_CODE_INVALID_INPUT,
exitCodeOnExecutionException = EXIT_CODE_EXECUTION_ERROR)
private static class App implements Callable {
diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java
index 24e7addac..c600be049 100644
--- a/src/main/java/picocli/CommandLine.java
+++ b/src/main/java/picocli/CommandLine.java
@@ -826,35 +826,6 @@ public List getUnmatchedArguments() {
return interpreter.parseResultBuilder == null ? Collections.emptyList() : UnmatchedArgumentException.stripErrorMessage(interpreter.parseResultBuilder.unmatched);
}
- /**
- * Sets the header and exit code descriptions to show in the Exit Code section of the usage help.
- * Callers may be interested in the {@link UsageMessageSpec#keyValuesMap(String...) keyValuesMap} method for creating a map from a list of {@code "key:value"} Strings.
- * The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its
- * subcommands and nested sub-subcommands at the moment this method is called. Subcommands added
- * later will have the default setting. To ensure a setting is applied to all
- * subcommands, call the setter last, after adding subcommands.
- *
- * @param exitCodeListHeading the heading preceding the exit codes section, ending in {@code "%n"}.
- * May contain additional {@code "%n"} line separators.
- * Common values are {@code "Exit Status%n"} or {@code "Exit Codes%n"}.
- * @param exitCodeDescriptions map with values to be displayed in the exit codes section;
- * each entry has an exit code key and its description as value.
- * Descriptions containing {@code "%n"} line separators are broken up into multiple lines.
- * @see UsageMessageSpec#keyValuesMap(String...)
- * @see UsageMessageSpec#exitCodeList()
- * @see UsageMessageSpec#exitCodeListHeading()
- * @see UsageMessageSpec#SECTION_KEY_EXIT_CODE_LIST
- * @see UsageMessageSpec#SECTION_KEY_EXIT_CODE_LIST_HEADING
- * @since 4.0 */
- public CommandLine setExitCodeHelpSection(String exitCodeListHeading, Map exitCodeDescriptions) {
- getCommandSpec().usageMessage().exitCodeListHeading(exitCodeListHeading);
- getCommandSpec().usageMessage().exitCodeList(exitCodeDescriptions);
- for (CommandLine command : getCommandSpec().subcommands().values()) {
- command.setExitCodeHelpSection(exitCodeListHeading, exitCodeDescriptions);
- }
- return this;
- }
-
/**
* Defines some exit codes used by picocli as default return values from the {@link #execute(String...) execute}
* and {@link #executeHelpRequest(ParseResult) executeHelpRequest} methods.
@@ -3709,10 +3680,15 @@ private static class NoCompletionCandidates implements Iterable {
* message. From 3.6, methods can also be annotated with {@code @Command}, where the method parameters define the
* command options and positional parameters.
*
- * @Command(name = "Encrypt", mixinStandardHelpOptions = true,
- * description = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
- * version = "Encrypt version 1.0",
- * footer = "Copyright (c) 2017")
+ * @Command(name = "Encrypt", mixinStandardHelpOptions = true,
+ * description = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
+ * version = "Encrypt version 1.0",
+ * footer = "Copyright (c) 2017",
+ * exitCodeListHeading = "Exit Codes:%n",
+ * exitCodeList = { " 0:Successful program execution.",
+ * "64:Invalid input: an unknown option or invalid parameter was specified.",
+ * "70:Execution exception: an exception occurred while executing the business logic."}
+ * )
* public class Encrypt {
* @Parameters(paramLabel = "FILE", description = "Any number of input files")
* private List<File> files = new ArrayList<File>();
@@ -3731,6 +3707,7 @@ private static class NoCompletionCandidates implements Iterable {
* [description]
* [parameter list]: {@code [FILE...] Any number of input files}
* [option list]: {@code -h, --help prints this help message and exits}
+ * [exit code list]
* [footer]
* */
@Retention(RetentionPolicy.RUNTIME)
@@ -3999,6 +3976,23 @@ private static class NoCompletionCandidates implements Iterable {
* @see #execute(String...)
* @since 4.0 */
int exitCodeOnExecutionException() default ExitCode.SOFTWARE;
+
+ /** Set the heading preceding the exit codes section, may contain {@code "%n"} line separators. {@code ""} (empty string) by default.
+ * @see Help#exitCodeListHeading(Object...)
+ * @since 4.0 */
+ String exitCodeListHeading() default "";
+
+ /** Set the values to be displayed in the exit codes section as a list of {@code "key:value"} pairs:
+ * keys are exit codes, values are descriptions. Descriptions may contain {@code "%n"} line separators.
+ * For example:
+ *
+ * @Command(exitCodeListHeading = "Exit Codes:%n",
+ * exitCodeList = { " 0:Successful program execution.",
+ * "64:Invalid input: an unknown option or invalid parameter was specified.",
+ * "70:Execution exception: an exception occurred while executing the business logic."})
+ *
+ * @since 4.0 */
+ String[] exitCodeList() default {};
}
/** A {@code Command} may define one or more {@code ArgGroups}: a group of options, positional parameters or a mixture of the two.
* Groups can be used to:
@@ -5537,6 +5531,7 @@ public static class UsageMessageSpec {
private String commandListHeading;
private String footerHeading;
private String exitCodeListHeading;
+ private String[] exitCodeListStrings;
private Map exitCodeList;
private int width = DEFAULT_USAGE_WIDTH;
@@ -5756,6 +5751,7 @@ private String[] arr(String[] localized, String[] value, String[] defaultValue)
/** Returns an unmodifiable map with values to be displayed in the exit codes section: keys are exit codes, values are descriptions.
* Descriptions may contain {@code "%n"} line separators.
+ * Callers may be interested in the {@link UsageMessageSpec#keyValuesMap(String...) keyValuesMap} method for creating a map from a list of {@code "key:value"} Strings.
* This may be configured in a resource bundle by listing up multiple {@code "key:value"} pairs. For example:
*
* usage.exitCodeList.0 = 0:Successful program execution.
@@ -5766,8 +5762,9 @@ private String[] arr(String[] localized, String[] value, String[] defaultValue)
* @see #keyValuesMap(String...)
* @since 4.0 */
public Map exitCodeList() {
- Map result = keyValuesMap(resourceArr("usage.exitCodeList"));
- return !result.isEmpty() ? Collections.unmodifiableMap(result) : (exitCodeList == null ? Collections.emptyMap() : exitCodeList);
+ Map result = keyValuesMap(arr(resourceArr("usage.exitCodeList"), exitCodeListStrings, DEFAULT_MULTI_LINE));
+ if (result != null) { return Collections.unmodifiableMap(result); }
+ return exitCodeList == null ? Collections.emptyMap() : exitCodeList;
}
/** Creates and returns a {@code Map} that contains an entry for each specified String that is in {@code "key:value"} format.
@@ -5901,6 +5898,9 @@ public static Map keyValuesMap(String... entries) {
public UsageMessageSpec adjustLineBreaksForWideCJKCharacters(boolean adjustForWideChars) { adjustLineBreaksForWideCJKCharacters = adjustForWideChars; return this; }
void updateFromCommand(Command cmd, CommandSpec commandSpec) {
+ if (!empty(cmd.resourceBundle())) { // else preserve superclass bundle
+ messages(new Messages(commandSpec, cmd.resourceBundle()));
+ }
if (isNonDefault(cmd.synopsisHeading(), DEFAULT_SYNOPSIS_HEADING)) {synopsisHeading = cmd.synopsisHeading();}
if (isNonDefault(cmd.commandListHeading(), DEFAULT_COMMAND_LIST_HEADING)) {commandListHeading = cmd.commandListHeading();}
if (isNonDefault(cmd.requiredOptionMarker(), DEFAULT_REQUIRED_OPTION_MARKER)) {requiredOptionMarker = cmd.requiredOptionMarker();}
@@ -5913,15 +5913,13 @@ void updateFromCommand(Command cmd, CommandSpec commandSpec) {
if (isNonDefault(cmd.descriptionHeading(), DEFAULT_SINGLE_VALUE)) {descriptionHeading = cmd.descriptionHeading();}
if (isNonDefault(cmd.header(), DEFAULT_MULTI_LINE)) {header = cmd.header().clone();}
if (isNonDefault(cmd.headerHeading(), DEFAULT_SINGLE_VALUE)) {headerHeading = cmd.headerHeading();}
+ if (isNonDefault(cmd.exitCodeList(), DEFAULT_MULTI_LINE)) {exitCodeListStrings = cmd.exitCodeList().clone();}
+ if (isNonDefault(cmd.exitCodeListHeading(), DEFAULT_SINGLE_VALUE)) {exitCodeListHeading = cmd.exitCodeListHeading();}
if (isNonDefault(cmd.footer(), DEFAULT_MULTI_LINE)) {footer = cmd.footer().clone();}
if (isNonDefault(cmd.footerHeading(), DEFAULT_SINGLE_VALUE)) {footerHeading = cmd.footerHeading();}
if (isNonDefault(cmd.parameterListHeading(), DEFAULT_SINGLE_VALUE)) {parameterListHeading = cmd.parameterListHeading();}
if (isNonDefault(cmd.optionListHeading(), DEFAULT_SINGLE_VALUE)) {optionListHeading = cmd.optionListHeading();}
if (isNonDefault(cmd.usageHelpWidth(), DEFAULT_USAGE_WIDTH)) {width(cmd.usageHelpWidth());} // validate
-
- if (!empty(cmd.resourceBundle())) { // else preserve superclass bundle
- messages(new Messages(commandSpec, cmd.resourceBundle()));
- }
}
void initFromMixin(UsageMessageSpec mixin, CommandSpec commandSpec) {
if (initializable(synopsisHeading, mixin.synopsisHeading(), DEFAULT_SYNOPSIS_HEADING)) {synopsisHeading = mixin.synopsisHeading();}
@@ -5936,6 +5934,8 @@ void initFromMixin(UsageMessageSpec mixin, CommandSpec commandSpec) {
if (initializable(descriptionHeading, mixin.descriptionHeading(), DEFAULT_SINGLE_VALUE)) {descriptionHeading = mixin.descriptionHeading();}
if (initializable(header, mixin.header(), DEFAULT_MULTI_LINE)) {header = mixin.header().clone();}
if (initializable(headerHeading, mixin.headerHeading(), DEFAULT_SINGLE_VALUE)) {headerHeading = mixin.headerHeading();}
+ if (initializable(exitCodeList, mixin.exitCodeList(), Collections.emptyMap()) && exitCodeListStrings == null) {exitCodeList = Collections.unmodifiableMap(new LinkedHashMap(mixin.exitCodeList()));}
+ if (initializable(exitCodeListHeading, mixin.exitCodeListHeading(), DEFAULT_SINGLE_VALUE)) {exitCodeListHeading = mixin.exitCodeListHeading();}
if (initializable(footer, mixin.footer(), DEFAULT_MULTI_LINE)) {footer = mixin.footer().clone();}
if (initializable(footerHeading, mixin.footerHeading(), DEFAULT_SINGLE_VALUE)) {footerHeading = mixin.footerHeading();}
if (initializable(parameterListHeading, mixin.parameterListHeading(), DEFAULT_SINGLE_VALUE)) {parameterListHeading = mixin.parameterListHeading();}
@@ -8952,19 +8952,20 @@ private String resolveLookups(String text, Set visited, Map= 0) { actualKey = fullKey.substring(0, defaultStartPos); }
- if (resolved.containsKey(prefix + actualKey)) { return resolved.get(prefix + actualKey); }
- if (visited.contains(prefix + actualKey)) {
+ String value = resolved.containsKey(prefix + actualKey)
+ ? resolved.get(prefix + actualKey)
+ : lookup.get(actualKey);
+ if (visited.contains(prefix + actualKey) && !resolved.containsKey(prefix + actualKey)) {
throw new InitializationException("Lookup '" + prefix + actualKey + "' has a circular reference.");
}
visited.add(prefix + actualKey);
- String value = lookup.get(actualKey);
if (value == null && defaultStartPos >= 0) {
String defaultValue = fullKey.substring(defaultStartPos + 2);
value = resolveLookups(defaultValue, visited, resolved);
}
resolved.put(prefix + actualKey, value);
if (value == null && startPos == 0 && endPos == text.length() - 1) {
- return null;
+ return null; // #676 x="${var}" should resolve to x=null if not found (not x="null")
}
// interpolate
diff --git a/src/test/java/picocli/ExecuteTest.java b/src/test/java/picocli/ExecuteTest.java
index c5e7574c5..a01a346b2 100644
--- a/src/test/java/picocli/ExecuteTest.java
+++ b/src/test/java/picocli/ExecuteTest.java
@@ -1012,24 +1012,19 @@ public TimeUnit call() {
}
@Test
- public void testSetExitCodeHelpSection() {
- @Command(mixinStandardHelpOptions = true)
+ public void testExitCodeListAnnotation() {
+ @Command(mixinStandardHelpOptions = true,
+ exitCodeListHeading = "Exit Codes:%n",
+ exitCodeList = {
+ " 0:Successful program execution",
+ "64:Usage error: user input for the command was incorrect, " +
+ "e.g., the wrong number of arguments, a bad flag, " +
+ "a bad syntax in a parameter, etc.",
+ "70:Internal software error: an exception occurred when invoking " +
+ "the business logic of this command."})
class App {}
CommandLine cmd = new CommandLine(new App());
String expected = String.format("" +
- "Usage: [-hV]%n" +
- " -h, --help Show this help message and exit.%n" +
- " -V, --version Print version information and exit.%n");
- assertEquals(expected, cmd.getUsageMessage());
-
- cmd.setExitCodeHelpSection("Exit Codes:%n",
- keyValuesMap(" 0:Successful program execution",
- "64:Usage error: user input for the command was incorrect, " +
- "e.g., the wrong number of arguments, a bad flag, " +
- "a bad syntax in a parameter, etc.",
- "70:Internal software error: an exception occurred when invoking " +
- "the business logic of this command."));
- expected = String.format("" +
"Usage: [-hV]%n" +
" -h, --help Show this help message and exit.%n" +
" -V, --version Print version information and exit.%n" +
@@ -1043,21 +1038,18 @@ class App {}
}
@Test
- public void testSetExitCodeHelpSectionSetsUsageMessageSpec() {
- @Command(mixinStandardHelpOptions = true)
+ public void testExitCodeListAnnotationSetsUsageMessageSpec() {
+ @Command(mixinStandardHelpOptions = true,
+ exitCodeListHeading = "My Exit Codes%n",
+ exitCodeList = {
+ " 0:Normal Execution",
+ "64:Invalid user input",
+ "70:Internal error"})
class App {}
CommandLine cmd = new CommandLine(new App());
CommandSpec spec = cmd.getCommandSpec();
UsageMessageSpec usage = spec.usageMessage();
- assertEquals("", usage.exitCodeListHeading());
- assertEquals(true, usage.exitCodeList().isEmpty());
-
- cmd.setExitCodeHelpSection("My Exit Codes%n",
- keyValuesMap(" 0:Normal Execution",
- "64:Invalid user input",
- "70:Internal error"));
-
assertEquals("My Exit Codes%n", usage.exitCodeListHeading());
assertEquals(3, usage.exitCodeList().size());
assertEquals("Invalid user input", usage.exitCodeList().get("64"));
@@ -1076,8 +1068,13 @@ class App {}
}
@Test
- public void testSetExitCodeHelpSectionReordered() {
- @Command(mixinStandardHelpOptions = true)
+ public void testExitCodeListAnnotationReordered() {
+ @Command(mixinStandardHelpOptions = true,
+ exitCodeListHeading = "My Exit Codes:%n",
+ exitCodeList = {
+ " 0:Normal Execution",
+ "64:Invalid user input",
+ "70:Internal error"})
class App {}
CommandLine cmd = new CommandLine(new App());
@@ -1089,11 +1086,9 @@ class App {}
cmd.setHelpSectionKeys(keys);
cmd.getCommandSpec().usageMessage().optionListHeading("Options:%n");
- cmd.setExitCodeHelpSection("Exit Codes:%n",
- keyValuesMap(" 0:Normal Execution", "64:Invalid user input", "70:Internal error"));
String expected = String.format("" +
"Usage: [-hV]%n" +
- "Exit Codes:%n" +
+ "My Exit Codes:%n" +
" 0 Normal Execution%n" +
" 64 Invalid user input%n" +
" 70 Internal error%n" +
@@ -1122,13 +1117,13 @@ class App {}
}
@Test
- public void testResourceBundleOverwritesSetExitCodeHelpSection() {
- @Command(resourceBundle = "picocli.exitcodes")
+ public void testResourceBundleOverwritesExitCodeListAnnotation() {
+ @Command(resourceBundle = "picocli.exitcodes",
+ exitCodeListHeading = "EXIT STATUS%n",
+ exitCodeList = {"000:IGNORED 1", "11:IGNORED 2"})
class App {}
CommandLine cmd = new CommandLine(new App());
- cmd.setExitCodeHelpSection("EXIT STATUS%n",
- keyValuesMap("000:IGNORED 1", "11:IGNORED 2"));
String expected = String.format("" +
"Usage: %n" +
@@ -1143,19 +1138,15 @@ class App {}
}
@Test
- public void testSetExitCodeHelpSectionAllowsNullHeader() {
- @Command
+ public void testExitCodeListAnnotationAllowsNullHeader() {
+ @Command(
+ exitCodeList = {
+ " 0:Normal Execution",
+ "64:Invalid user input",
+ "70:Internal error"})
class App {}
CommandLine cmd = new CommandLine(new App());
String expected = String.format("" +
- "Usage: %n");
- assertEquals(expected, cmd.getUsageMessage());
-
- cmd.setExitCodeHelpSection(null,
- keyValuesMap(" 0:Normal Execution",
- "64:Invalid user input",
- "70:Internal error"));
- expected = String.format("" +
"Usage: %n" +
" 0 Normal Execution%n" +
" 64 Invalid user input%n" +
@@ -1164,31 +1155,86 @@ class App {}
}
@Test
- public void testSetExitCodeHelpSectionAllowsNullMap() {
- @Command
+ public void testExitCodeListAnnotationAllowsNullMap() {
+ @Command(exitCodeListHeading = "Exit Codes%n")
class App {}
CommandLine cmd = new CommandLine(new App());
+
String expected = String.format("" +
- "Usage: %n");
+ "Usage: %n" +
+ "Exit Codes%n");
assertEquals(expected, cmd.getUsageMessage());
+ }
+
+ @Test
+ public void testExitCodeListAnnotationKeyVariableInterpolation() {
+ @Command(exitCodeListHeading = "My ${sys:HEADING} Exit Codes:%n",
+ exitCodeList = {
+ "${sys:NORMAL}:Normal Execution",
+ "${sys:INVALID}:Invalid user input",
+ "${sys:INTERNAL}:Internal error"})
+ class App {}
+
+ System.setProperty("HEADING", "wonderful");
+ System.setProperty("NORMAL", "0000");
+ System.setProperty("INVALID", "1111");
+ System.setProperty("INTERNAL", "2222");
+ CommandLine cmd = new CommandLine(new App());
- cmd.setExitCodeHelpSection("Exit Codes%n", null);
- expected = String.format("" +
+ String expected = String.format("" +
"Usage: %n" +
- "Exit Codes%n");
+ "My wonderful Exit Codes:%n" +
+ " 0000 Normal Execution%n" +
+ " 1111 Invalid user input%n" +
+ " 2222 Internal error%n");
assertEquals(expected, cmd.getUsageMessage());
}
@Test
- public void testSetExitCodeHelpSectionAllowsNullHeaderAndMap() {
- @Command
+ public void testExitCodeListAnnotationDescriptionVariableInterpolation() {
+ @Command(exitCodeListHeading = "My ${sys:HEADING} Exit Codes:%n",
+ exitCodeList = {
+ " 0:Normal Execution (value is ${sys:NORMAL})",
+ "64:Invalid user input (value is ${sys:INVALID})",
+ "74:Internal error (value is ${sys:INTERNAL})"})
class App {}
+
+ System.setProperty("HEADING", "wonderful");
+ System.setProperty("NORMAL", "0000");
+ System.setProperty("INVALID", "1111");
+ System.setProperty("INTERNAL", "2222");
CommandLine cmd = new CommandLine(new App());
+
String expected = String.format("" +
- "Usage: %n");
+ "Usage: %n" +
+ "My wonderful Exit Codes:%n" +
+ " 0 Normal Execution (value is 0000)%n" +
+ " 64 Invalid user input (value is 1111)%n" +
+ " 74 Internal error (value is 2222)%n");
assertEquals(expected, cmd.getUsageMessage());
+ }
+
+ @Test
+ public void testExitCodeListAnnotationBothKeyAndDescriptionVariableInterpolation() {
+ @Command(exitCodeListHeading = "My ${sys:HEADING} Exit Codes:%n",
+ exitCodeList = {
+ "${sys:NORMAL}:Normal Execution (value is ${sys:NORMAL})",
+ "${sys:INVALID}:Invalid user input (value is ${sys:INVALID})",
+ "${sys:INTERNAL}:Internal error (value is ${sys:INTERNAL})"})
+ class App {}
+
+ System.setProperty("HEADING", "wonderful");
+ System.setProperty("NORMAL", "0000");
+ System.setProperty("INVALID", "1111");
+ System.setProperty("INTERNAL", "2222");
+ CommandLine cmd = new CommandLine(new App());
- cmd.setExitCodeHelpSection(null, null);
+ String expected = String.format("" +
+ "Usage: %n" +
+ "My wonderful Exit Codes:%n" +
+ " 0000 Normal Execution (value is 0000)%n" +
+ " 1111 Invalid user input (value is 1111)%n" +
+ " 2222 Internal error (value is 2222)%n");
assertEquals(expected, cmd.getUsageMessage());
}
diff --git a/src/test/java/picocli/I18nSuperclass.java b/src/test/java/picocli/I18nSuperclass.java
index 37cf8b66a..74059fa5a 100644
--- a/src/test/java/picocli/I18nSuperclass.java
+++ b/src/test/java/picocli/I18nSuperclass.java
@@ -16,7 +16,10 @@
footerHeading = "super footer heading%n",
commandListHeading = "super command list heading%n",
optionListHeading = "super option list heading%n",
- parameterListHeading = "super param list heading%n")
+ parameterListHeading = "super param list heading%n",
+ exitCodeListHeading = "super exit code list heading%n",
+ exitCodeList = {"000:super exit code 1", "111:super exit code 2"}
+)
public class I18nSuperclass {
@Option(names = {"-x", "--xxx"})
String x;
diff --git a/src/test/java/picocli/I18nTest.java b/src/test/java/picocli/I18nTest.java
index e68159770..7f3a92b20 100644
--- a/src/test/java/picocli/I18nTest.java
+++ b/src/test/java/picocli/I18nTest.java
@@ -77,6 +77,11 @@ public void testSuperclassWithResourceBundle() {
"%n" +
"Commands from bundle:%n" +
" help header first line from bundle%n" +
+ "Exit Codes:%n" +
+ "This exit code description comes from top bundle%n" +
+ " 0 (top bundle) Normal termination (notice leading space)%n" +
+ " 64 (top bundle) Invalid input%n" +
+ " 70 (top bundle) internal error%n" +
"Powered by picocli from bundle%n" +
"footer from bundle%n");
assertEquals(expected, new CommandLine(new I18nSuperclass()).getUsageMessage());
@@ -118,6 +123,11 @@ public void testSubclassInheritsSuperResourceBundle() {
"%n" +
"Commands from bundle:%n" +
" help header first line from bundle%n" +
+ "Exit Codes:%n" +
+ "This exit code description comes from top bundle%n" +
+ " 0 (top bundle) Normal termination (notice leading space)%n" +
+ " 64 (top bundle) Invalid input%n" +
+ " 70 (top bundle) internal error%n" +
"Powered by picocli from bundle%n" +
"footer from bundle%n");
assertEquals(expected, new CommandLine(new I18nSubclass()).getUsageMessage());
@@ -157,6 +167,9 @@ public void testSubclassBundleOverridesSuperBundle() {
// not "header line from subbundle":
// help command is inherited from superclass, initialized with resource bundle from superclass
" help header first line from bundle%n" +
+ "super exit code list heading%n" +
+ " 000 super exit code 1%n" +
+ " 111 super exit code 2%n" +
"sub footer heading from subbundle%n" +
"sub footer from subbundle%n");
System.setProperty("picocli.trace", "DEBUG");
diff --git a/src/test/java/picocli/InterpolatorTest.java b/src/test/java/picocli/InterpolatorTest.java
index 98bdf7a5f..d750b64a2 100644
--- a/src/test/java/picocli/InterpolatorTest.java
+++ b/src/test/java/picocli/InterpolatorTest.java
@@ -1,7 +1,10 @@
package picocli;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.contrib.java.lang.system.RestoreSystemProperties;
+import org.junit.rules.TestRule;
import picocli.CommandLine.Model.Interpolator;
import picocli.CommandLine.Model.CommandSpec;
@@ -17,6 +20,11 @@
import static org.junit.Assert.*;
public class InterpolatorTest {
+
+ @Rule
+ // allows tests to set any kind of properties they like, without having to individually roll them back
+ public final TestRule restoreSystemProperties = new RestoreSystemProperties();
+
@Test
public void interpolateCommandName() {
CommandSpec hierarchy = createTestSpec();
@@ -183,6 +191,18 @@ public void interpolateSystemPropertyWithMultipleSequentialLookupsInDefaultSecon
System.clearProperty("second");
}
+ @Test
+ public void interpolateMultipleOccurrences() {
+ CommandSpec hierarchy = createTestSpec();
+ Interpolator interpolator = new Interpolator(hierarchy);
+ String original = "abc ${sys:key} def ${sys:key}.";
+ String expected = "abc 111 def 111.";
+
+ System.setProperty("key", "111");
+ assertEquals(expected, interpolator.interpolate(original));
+ System.clearProperty("key");
+ }
+
private CommandSpec createTestSpec() {
CommandSpec result = CommandSpec.create().name("top")
.addSubcommand("sub", CommandSpec.create().name("sub")
diff --git a/src/test/resources/picocli/I18nSuperclass_Messages.properties b/src/test/resources/picocli/I18nSuperclass_Messages.properties
index 035da4776..1f7acc015 100644
--- a/src/test/resources/picocli/I18nSuperclass_Messages.properties
+++ b/src/test/resources/picocli/I18nSuperclass_Messages.properties
@@ -19,6 +19,10 @@ usage.parameterListHeading = %nPositional parameters from bundle:%n
usage.optionListHeading = %nOptions from bundle:%n
usage.commandListHeading = %nCommands from bundle:%n
usage.footerHeading = Powered by picocli from bundle%n
+usage.exitCodeListHeading = Exit Codes:%nThis exit code description comes from top bundle%n
+usage.exitCodeList.0 = \u00200:(top bundle) Normal termination (notice leading space)
+usage.exitCodeList.1 = 64:(top bundle) Invalid input
+usage.exitCodeList.2 = 70:(top bundle) internal error
# Option Descriptions
# -------------------