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 # -------------------