diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6b3e26210..a89951427 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -80,11 +80,24 @@ When abbreviated options are enabled, user input `-AB` will match the long `-Aaa ## Fixed issues * [#10][#732][#1047] API: Support abbreviated options and commands. Thanks to [NewbieOrange](https://github.com/NewbieOrange) for the pull request. * [#1074][#1075] API: Added method `ParseResult::expandedArgs` to return the list of arguments after `@-file` expansion. Thanks to [Kevin Bedi](https://github.com/mashlol) for the pull request. +* [#1052] API: Show/Hide commands in usage help on specific conditions. Thanks to [Philippe Charles](https://github.com/charphi) for raising this. +* [#1088] API: Add method `Help::allSubcommands` to return all subcommands, including hidden ones. Clarify the semantics of `Help::subcommands`. +* [#1090] API: Add methods `Help::optionListExcludingGroups` to return a String with the rendered section of the usage help containing only the specified options, including hidden ones. +* [#1092] API: Add method `Help::parameterList(List)` to return a String with the rendered section of the usage help containing only the specified positional parameters, including hidden ones. +* [#1093] API: Add method `Help::commandList(Map)` to return a String with the rendered section of the usage help containing only the specified subcommands, including hidden ones. +* [#1091] API: Add method `Help::optionListGroupSections` to return a String with the rendered section of the usage help containing only the option groups. +* [#1089] API: Add method `Help::createDefaultOptionSort` to create a `Comparator` that follows the command and options' configuration. +* [#1084][#1094] API: Add method `Help::createDefaultLayout(List, List, ColorScheme)` to create a layout for the specified options and positionals. +* [#1087] API: Add methods `ArgumentGroupSpec::allOptionsNested` and `ArgumentGroupSpec::allPositionalParametersNested`. +* [#1086] API: add methods `Help.Layout::addAllOptions` and `Help.Layout::addAllPositionals`, to show all specified options, including hidden ones. +* [#1085] API: Add method `Help::optionSectionGroups` to get argument groups with a header. * [#1051][#1056] Enhancement: `GenerateCompletion` command no longer needs to be a direct subcommand of the root command. Thanks to [Philippe Charles](https://github.com/charphi) for the pull request. * [#1068] Enhancement: Make `ParserSpec::toString` output settings in alphabetic order. * [#1069] Enhancement: Debug output should show `optionsCaseInsensitive` and `subcommandsCaseInsensitive` settings. * [#1070] Enhancement: Code cleanup: removed redundant modifiers and initializations, unused variables, incorrect javadoc references, and more. Thanks to [NewbieOrange](https://github.com/NewbieOrange) for the pull request. +* [#1096] Enhancement: Override `Help.Column` `equals`, `hashCode` and `toString` methods to facilitate testing. * [#1063][#1064] `ManPageGenerator` now correctly excludes hidden options, parameters, and subcommands from man page generation. Thanks to [Brian Demers](https://github.com/bdemers) for the pull request. +* [#1081] Bugfix: `CommandLine.Help` constructor no longer calls overridable methods `addAllSubcommands` and `createDefaultParamLabelRenderer`. * [#1065] Bugfix: With a `List<>` option in `@ArgGroup`, group incorrectly appears twice in the synopsis. Thanks to [kap4lin](https://github.com/kap4lin) for raising this. * [#1067] Bugfix: `ParserSpec::initFrom` was not copying `useSimplifiedAtFiles`. * [#1054] Bugfix: option-parameter gets lost in Argument Groups. Thanks to [waacc-gh](https://github.com/waacc-gh) for raising this. diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index e03aeda6b..08854e3f9 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -9553,6 +9553,32 @@ public List options() { for (ArgSpec arg : args()) { if (arg instanceof OptionSpec) { result.add((OptionSpec) arg); } } return Collections.unmodifiableList(result); } + /** Returns all options configured for this group and all subgroups. + * @return an immutable list of all options in this group and its subgroups. + * @since 4.4 */ + public List allOptionsNested() { + return addGroupOptionsToListRecursively(new ArrayList()); + } + private List addGroupOptionsToListRecursively(List result) { + result.addAll(options()); + for (ArgGroupSpec subGroup : subgroups()) { + subGroup.addGroupOptionsToListRecursively(result); + } + return result; + } + /** Returns all positional parameters configured for this group and all subgroups. + * @return an immutable list of all positional parameters in this group and its subgroups. + * @since 4.4 */ + public List allPositionalParametersNested() { + return addGroupPositionalsToListRecursively(new ArrayList()); + } + private List addGroupPositionalsToListRecursively(List result) { + result.addAll(positionalParameters()); + for (ArgGroupSpec subGroup : subgroups()) { + subGroup.addGroupPositionalsToListRecursively(result); + } + return result; + } /** Returns the synopsis of this group. */ public String synopsis() { @@ -13971,7 +13997,8 @@ private OptionSpec createEndOfOptionsOption(String name) { private final CommandSpec commandSpec; private final ColorScheme colorScheme; - private final Map commands = new LinkedHashMap(); + private final Map allCommands = new LinkedHashMap(); + private final Map visibleCommands = new LinkedHashMap(); private List aliases; private final IParamLabelRenderer parameterLabelRenderer; @@ -14007,9 +14034,9 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) { this.aliases = new ArrayList(Arrays.asList(commandSpec.aliases())); this.aliases.add(0, commandSpec.name()); this.colorScheme = new ColorScheme.Builder(colorScheme).applySystemProperties().build(); - parameterLabelRenderer = createDefaultParamLabelRenderer(); // uses help separator + this.parameterLabelRenderer = new DefaultParamLabelRenderer(commandSpec); // uses help separator - this.addAllSubcommands(commandSpec.subcommands()); + this.registerSubcommands(commandSpec.subcommands()); AT_FILE_POSITIONAL_PARAM.commandSpec = commandSpec; // for interpolation } @@ -14027,9 +14054,15 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) { * @since 3.9 */ private IHelpFactory getHelpFactory() { return commandSpec.usageMessage().helpFactory(); } - /** Returns the map of subcommand {@code Help} instances for this command Help. - * @since 3.9 */ - public Map subcommands() { return Collections.unmodifiableMap(commands); } + /** Returns the map of non-hidden subcommand {@code Help} instances for this command Help. + * @since 3.9 + * @see #allSubcommands() */ + public Map subcommands() { return Collections.unmodifiableMap(visibleCommands); } + + /** Returns the map of all subcommand {@code Help} instances (including hidden commands) for this command Help. + * @since 4.4 + * @see #subcommands() */ + public Map allSubcommands() { return Collections.unmodifiableMap(allCommands); } /** Returns the list of aliases for the command in this Help. * @since 3.9 */ @@ -14042,59 +14075,60 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) { public IParamLabelRenderer parameterLabelRenderer() {return parameterLabelRenderer;} /** Registers all specified subcommands with this Help. - * @param commands maps the command names to the associated CommandLine object + * @param subcommands the subcommands of this command * @return this Help instance (for method chaining) - * @see CommandLine#getSubcommands() + * @see #subcommands() + * @see #allSubcommands() */ - public Help addAllSubcommands(Map commands) { - if (commands != null) { - // first collect aliases - Map> done = new IdentityHashMap>(); - for (CommandLine cmd : commands.values()) { - if (!done.containsKey(cmd)) { - done.put(cmd, new ArrayList(Arrays.asList(cmd.commandSpec.aliases()))); - } - } - // then loop over all names that the command was registered with and add this name to the front of the list (if it isn't already in the list) - for (Map.Entry entry : commands.entrySet()) { - List aliases = done.get(entry.getValue()); - if (!aliases.contains(entry.getKey())) { aliases.add(0, entry.getKey()); } - } - // The aliases list for each command now has at least one entry, with the main name at the front. - // Now we loop over the commands in the order that they were registered on their parent command. - for (Map.Entry entry : commands.entrySet()) { - // not registering hidden commands is easier than suppressing display in Help.commandList(): - // if all subcommands are hidden, help should not show command list header - if (!entry.getValue().getCommandSpec().usageMessage().hidden()) { - List aliases = done.remove(entry.getValue()); - if (aliases != null) { // otherwise we already processed this command by another alias - addSubcommand(aliases, entry.getValue()); - } - } - } + public Help addAllSubcommands(Map subcommands) { + if (subcommands != null) { + registerSubcommands(subcommands); } return this; } - /** Registers the specified subcommand with this Help. - * @param commandNames the name and aliases of the subcommand to display in the usage message - * @param commandLine the {@code CommandLine} object to get more information from - * @return this Help instance (for method chaining) */ - Help addSubcommand(List commandNames, CommandLine commandLine) { - String all = commandNames.toString(); - commands.put(all.substring(1, all.length() - 1), getHelpFactory().create(commandLine.commandSpec, colorScheme).withCommandNames(commandNames)); - return this; + private void registerSubcommands(Map subcommands) { + // first collect aliases + Map> done = new IdentityHashMap>(); + for (CommandLine cmd : subcommands.values()) { + if (!done.containsKey(cmd)) { + done.put(cmd, new ArrayList(Arrays.asList(cmd.commandSpec.aliases()))); + } + } + // then loop over all names that the command was registered with and add this name to the front of the list (if it isn't already in the list) + for (Map.Entry entry : subcommands.entrySet()) { + List aliases = done.get(entry.getValue()); + if (!aliases.contains(entry.getKey())) { aliases.add(0, entry.getKey()); } + } + // The aliases list for each command now has at least one entry, with the main name at the front. + // Now we loop over the commands in the order that they were registered on their parent command. + for (Map.Entry entry : subcommands.entrySet()) { + // not registering hidden commands is easier than suppressing display in Help.commandList(): + // if all subcommands are hidden, help should not show command list header + CommandLine commandLine = entry.getValue(); + List commandNames = done.remove(commandLine); + if (commandNames == null) { continue; } // we already processed this command by another alias + String key = commandNames.toString().substring(1, commandNames.toString().length() - 1); + Help sub = getHelpFactory().create(commandLine.commandSpec, colorScheme).withCommandNames(commandNames); + allCommands.put(key, sub); + if (!sub.commandSpec().usageMessage().hidden()) { + visibleCommands.put(key, sub); + } + } } - /** Registers the specified subcommand with this Help. + /** Registers the specified subcommand as one of the visible commands in this Help. + * This method does not check whether the specified command is hidden or not. * @param commandName the name of the subcommand to display in the usage message * @param command the {@code CommandSpec} or {@code @Command} annotated object to get more information from * @return this Help instance (for method chaining) + * @see #subcommands() * @deprecated */ @Deprecated public Help addSubcommand(String commandName, Object command) { - commands.put(commandName, - getHelpFactory().create(CommandSpec.forAnnotatedObject(command, commandSpec.commandLine().factory), defaultColorScheme(Ansi.AUTO))); + Help sub = getHelpFactory().create(CommandSpec.forAnnotatedObject(command, commandSpec.commandLine().factory), defaultColorScheme(Ansi.AUTO)); + visibleCommands.put(commandName, sub); + allCommands.put(commandName, sub); return this; } @@ -14362,20 +14396,25 @@ public int synopsisHeadingLength() { String[] lines = Ansi.OFF.new Text(commandSpec.usageMessage().synopsisHeading()).toString().split("\\r?\\n|\\r|%n", -1); return lines[lines.length - 1].length(); } - /** - *

Returns a description of the {@linkplain Option options} supported by the application. - * This implementation {@linkplain #createShortOptionNameComparator() sorts options alphabetically}, and shows - * only the {@linkplain Option#hidden() non-hidden} options in a {@linkplain TextTable tabular format} - * using the {@linkplain #createDefaultOptionRenderer() default renderer} and {@linkplain Layout default layout}.

- * @return the fully formatted option list - * @see #optionList(Layout, Comparator, IParamLabelRenderer) - */ - public String optionList() { - Comparator sortOrder = commandSpec.usageMessage().sortOptions() - ? createShortOptionNameComparator() - : createOrderComparatorIfNecessary(commandSpec.options()); - - return optionList(createDefaultLayout(), sortOrder, parameterLabelRenderer()); + private List excludeHiddenAndGroupOptions(List all) { + List result = new ArrayList(all); + for (ArgGroupSpec group : optionSectionGroups()) { result.removeAll(group.allOptionsNested()); } + for (Iterator iter = result.iterator(); iter.hasNext(); ) { + if (iter.next().hidden()) { + iter.remove(); + } + } + return result; + } + private List excludeHiddenAndGroupParams(List all) { + List result = new ArrayList(all); + for (ArgGroupSpec group : optionSectionGroups()) { result.removeAll(group.allPositionalParametersNested()); } + for (Iterator iter = result.iterator(); iter.hasNext(); ) { + if (iter.next().hidden()) { + iter.remove(); + } + } + return result; } private static Comparator createOrderComparatorIfNecessary(List options) { @@ -14383,135 +14422,174 @@ private static Comparator createOrderComparatorIfNecessary(List positionals = new ArrayList(commandSpec.positionalParameters()); // iterate in declaration order - if (hasAtFileParameter()) { - positionals.add(0, AT_FILE_POSITIONAL_PARAM); - AT_FILE_POSITIONAL_PARAM.messages(commandSpec.usageMessage().messages()); - } - //IParameterRenderer paramRenderer = new DefaultParameterRenderer(false, " "); - for (PositionalParamSpec positional : positionals) { - if (positional.hidden()) { continue; } - //Text[][] values = paramRenderer.render(positional, parameterLabelRenderer(), colorScheme); // values[0][3]; // - Text label = parameterLabelRenderer().renderParameterLabel(positional, colorScheme.ansi(), colorScheme.parameterStyles); - int len = cjk ? label.getCJKAdjustedLength() : label.length; - if (len < longOptionsColWidth) { max = Math.max(max, len); } - } - - return max + 3; + /** + * Returns a comparator for sorting options, or {@code null}, depending on the settings for this command. + * @return if {@linkplain Command#sortOptions() sortOptions} is selected, + * return a comparator for sorting options based on their short name. + * Otherwise, if any of the options has a non-default value for their {@linkplain Option#order() order} attribute, + * then return a comparator for sorting options based on the order attribute. + * Otherwise, return {@code null} to indicate that options should not be sorted. + * @since 4.4 + */ + public Comparator createDefaultOptionSort() { + return commandSpec.usageMessage().sortOptions() + ? createShortOptionNameComparator() + : createOrderComparatorIfNecessary(commandSpec.options()); } - /** - * Add options in the specified group (or in its subgroup) recursively to the specified option list. - * @param group current group to deal with - * @param options global result where options of current group will be added to - * @param optionSort comparator for options + *

Returns a description of {@linkplain #options() all options} in this command, including any argument groups. + *

+ * This implementation {@linkplain #createShortOptionNameComparator() sorts options alphabetically}, and shows + * only the {@linkplain Option#hidden() non-hidden} options in a {@linkplain TextTable tabular format} + * using the {@linkplain #createDefaultOptionRenderer() default renderer} and {@linkplain Layout default layout}.

+ * @return the fully formatted option list, including any argument groups + * @see #optionListExcludingGroups(List) + * @see #optionListGroupSections() */ - private void addGroupOptionsToListRecursively(ArgGroupSpec group, List options, Comparator optionSort) { - if (group == null) { return; } - List tmp = new ArrayList(group.options()); - if (optionSort != null) { - Collections.sort(tmp, optionSort); - } - options.addAll(tmp); - for (ArgGroupSpec subGroup : group.subgroups()) { - addGroupOptionsToListRecursively(subGroup, options, optionSort); - } + public String optionList() { + return optionList(createDefaultLayout(), createDefaultOptionSort(), parameterLabelRenderer()); } - /** - * Remove options in the specified group (or in its subgroup) recursively from the specified option list. - * @param group current group to deal with - * @param options global result where options of current group will be removed from + *

Returns a description of the specified list of options. + *

+ * This implementation {@linkplain #createShortOptionNameComparator() sorts options alphabetically}, and shows + * only the specified options in a {@linkplain TextTable tabular format} + * using the {@linkplain #createDefaultOptionRenderer() default renderer} and {@linkplain #createDefaultLayout(List, List, ColorScheme)} default layout}. + *

+ * Argument groups are not rendered by this method. + *

+ * @param options the options to display in the returned rendered section of the usage help message + * @return the fully formatted portion of the option list for the specified options only (argument groups are not included) + * @see #optionListExcludingGroups(List, Layout, Comparator, IParamLabelRenderer) + * @since 4.4 */ - private void removeGroupOptionsFromListRecursively(ArgGroupSpec group, List options) { - if (group == null) { return; } - options.removeAll(group.options()); - for (ArgGroupSpec subGroup : group.subgroups()) { - removeGroupOptionsFromListRecursively(subGroup, options); - } + public String optionListExcludingGroups(List options) { + return optionListExcludingGroups(options, createDefaultLayout(), createDefaultOptionSort(), parameterLabelRenderer()); } - /** Sorts all {@code Options} with the specified {@code comparator} (if the comparator is non-{@code null}), * then {@linkplain Layout#addOption(CommandLine.Model.OptionSpec, CommandLine.Help.IParamLabelRenderer) adds} all non-hidden options to the * specified TextTable and returns the result of TextTable.toString(). - * @param layout responsible for rendering the option list + * @param layout the layout responsible for rendering the option list * @param valueLabelRenderer used for options with a parameter - * @return the fully formatted option list + * @return the fully formatted option list, including any argument groups + * @see #optionListExcludingGroups(List, Layout, Comparator, IParamLabelRenderer) + * @see #optionListGroupSections() * @since 3.0 */ public String optionList(Layout layout, Comparator optionSort, IParamLabelRenderer valueLabelRenderer) { - List options = new ArrayList(commandSpec.options()); // options are stored in order of declaration + List visibleOptionsNotInGroups = excludeHiddenAndGroupOptions(options()); + return optionListExcludingGroups(visibleOptionsNotInGroups, layout, optionSort, valueLabelRenderer) + optionListGroupSections(); + } + /** Sorts all {@code Options} with the specified {@code comparator} (if the comparator is non-{@code null}), + * then {@linkplain Layout#addOption(CommandLine.Model.OptionSpec, CommandLine.Help.IParamLabelRenderer) adds} the specified options to the + * specified TextTable and returns the result of TextTable.toString(). + * Argument groups are not rendered by this method. + * @param optionList the options to show (this may be a subset of the options in this command); + * it is the responsibility of the caller to remove options that should not be displayed + * @param layout the layout responsible for rendering the option list + * @param valueLabelRenderer used for options with a parameter + * @return the fully formatted portion of the option list for the specified options only (argument groups are not included) + * @since 4.4 */ + public String optionListExcludingGroups(List optionList, Layout layout, Comparator optionSort, IParamLabelRenderer valueLabelRenderer) { + List options = new ArrayList(optionList); // options are stored in order of declaration if (optionSort != null) { Collections.sort(options, optionSort); // default: sort options ABC } - List groups = optionListGroups(); - for (ArgGroupSpec group : groups) { - removeGroupOptionsFromListRecursively(group, options); - } - StringBuilder sb = new StringBuilder(); - layout.addOptions(options, valueLabelRenderer); - sb.append(layout.toString()); + layout.addAllOptions(options, valueLabelRenderer); + return layout.toString(); + } + /** + * Returns a rendered section of the usage help message that contains the argument groups that have a non-{@code null} {@linkplain ArgGroup#heading() heading}. + * This is usually shown below the "normal" options of the command (that are not in an argument group). + * @return the fully formatted portion of the option list showing the argument groups + * @since 4.4 + * @see #optionList() + * @see #optionListExcludingGroups(List) + * @see #optionSectionGroups() + */ + public String optionListGroupSections() { + return optionListGroupSections(optionSectionGroups(), createDefaultOptionSort(), parameterLabelRenderer()); + } + // @since 4.4 + private String optionListGroupSections(List groupList, Comparator optionSort, IParamLabelRenderer paramLabelRenderer) { Set done = new HashSet(); + List groups = new ArrayList(groupList); Collections.sort(groups, new SortByOrder()); - for (ArgGroupSpec group : groups) { - sb.append(createHeading(group.heading())); - Layout groupLayout = createDefaultLayout(); - groupLayout.addPositionalParameters(group.positionalParameters(), valueLabelRenderer); - List groupOptions = new ArrayList(); - addGroupOptionsToListRecursively(group, groupOptions, optionSort); + StringBuilder sb = new StringBuilder(); + for (ArgGroupSpec group : groups) { + List groupOptions = new ArrayList(group.allOptionsNested()); + if (optionSort != null) { Collections.sort(groupOptions, optionSort); } groupOptions.removeAll(done); done.addAll(groupOptions); - groupLayout.addOptions(groupOptions, valueLabelRenderer); + + List groupPositionals = new ArrayList(group.allPositionalParametersNested()); + groupPositionals.removeAll(done); + done.addAll(groupPositionals); + + Layout groupLayout = createDefaultLayout(); // create new empty layout based on the length of all options/positionals in this command + groupLayout.addPositionalParameters(groupPositionals, paramLabelRenderer); + groupLayout.addOptions(groupOptions, paramLabelRenderer); + + sb.append(createHeading(group.heading())); sb.append(groupLayout); } return sb.toString(); } - /** Returns the list of {@code ArgGroupSpec}s with a non-{@code null} heading. */ - private List optionListGroups() { + /** Returns the list of {@code ArgGroupSpec} instances in this command that have a non-{@code null} heading, most deeply nested argument groups first. + * @see #optionListGroupSections() + * @since 4.4 */ + public List optionSectionGroups() { List result = new ArrayList(); - optionListGroups(commandSpec.argGroups(), result); + optionSectionGroups(commandSpec.argGroups(), result); return result; } - private static void optionListGroups(List groups, List result) { + private static void optionSectionGroups(List groups, List result) { for (ArgGroupSpec group : groups) { - optionListGroups(group.subgroups(), result); + optionSectionGroups(group.subgroups(), result); if (group.heading() != null) { result.add(group); } } } /** - * Returns the section of the usage help message that lists the parameters with their descriptions. + * Returns the rendered positional parameters section of the usage help message for {@linkplain #positionalParameters() all positional parameters} in this command. * @return the section of the usage help message that lists the parameters + * @see #parameterList(List) */ public String parameterList() { - return parameterList(createDefaultLayout(), parameterLabelRenderer()); + return parameterList(excludeHiddenAndGroupParams(positionalParameters())); } /** - * Returns the section of the usage help message that lists the parameters with their descriptions. + * Returns the rendered positional parameters section of the usage help message for the specified positional parameters. + * @param positionalParams the positional parameters to display in the returned rendered section of the usage help message; + * the caller is responsible for removing parameters that should not be displayed + * @return the section of the usage help message that lists the parameters + * @see #parameterList(List, Layout, IParamLabelRenderer) + * @since 4.4 */ + public String parameterList(List positionalParams) { + return parameterList(positionalParams, createDefaultLayout(), parameterLabelRenderer()); + } + /** + * Returns the rendered section of the usage help message that lists all positional parameters in this command with their descriptions. * @param layout the layout to use * @param paramLabelRenderer for rendering parameter names * @return the section of the usage help message that lists the parameters */ public String parameterList(Layout layout, IParamLabelRenderer paramLabelRenderer) { - List positionals = new ArrayList(commandSpec.positionalParameters()); - List groups = optionListGroups(); - for (ArgGroupSpec group : groups) { positionals.removeAll(group.positionalParameters()); } - - layout.addPositionalParameters(positionals, paramLabelRenderer); + return parameterList(excludeHiddenAndGroupParams(positionalParameters()), layout, paramLabelRenderer); + } + /** + * Returns the rendered section of the usage help message that lists the specified parameters with their descriptions. + * @param positionalParams the positional parameters to display in the returned rendered section of the usage help message; + * the caller is responsible for removing parameters that should not be displayed + * @param layout the layout to use + * @param paramLabelRenderer for rendering parameter names + * @return the section of the usage help message that lists the parameters + * @since 4.4 */ + public String parameterList(List positionalParams, Layout layout, IParamLabelRenderer paramLabelRenderer) { + layout.addAllPositionalParameters(positionalParams, paramLabelRenderer); return layout.toString(); } @@ -14692,7 +14770,7 @@ public String optionListHeading(Object... params) { * @param params the parameters to use to format the command list heading * @return the formatted command list heading */ public String commandListHeading(Object... params) { - return commands.isEmpty() ? "" : createHeading(commandSpec.usageMessage().commandListHeading(), params); + return visibleCommands.isEmpty() ? "" : createHeading(commandSpec.usageMessage().commandListHeading(), params); } /** Returns the text displayed before the footer text; the result of {@code String.format(footerHeading, params)}. @@ -14760,23 +14838,32 @@ public TextTable createTextTable(Map map) { } return textTable; } - /** Returns a 2-column list with command names and the first line of their header or (if absent) description. - * @return a usage help section describing the added commands */ + /** Returns a 2-column list with the command names and first line of their header or (if absent) description of the commands returned by {@link #subcommands()}. + * @return a usage help section describing the added commands + * @see #commandList(Map) */ public String commandList() { - if (subcommands().isEmpty()) { return ""; } - int commandLength = maxLength(subcommands().keySet()); - Help.TextTable textTable = Help.TextTable.forColumns(ansi(), + return commandList(subcommands()); + } + /** Returns a 2-column list with the command names and first line of their header or (if absent) description of the specified command map. + * @return a usage help section describing the added commands + * @see #subcommands() + * @see #allSubcommands() + * @since 4.4 */ + public String commandList(Map subcommands) { + if (subcommands.isEmpty()) { return ""; } + int commandLength = maxLength(subcommands.keySet()); + Help.TextTable textTable = Help.TextTable.forColumns(colorScheme().ansi(), new Help.Column(commandLength + 2, 2, Help.Column.Overflow.SPAN), new Help.Column(width() - (commandLength + 2), 2, Help.Column.Overflow.WRAP)); textTable.setAdjustLineBreaksForWideCJKCharacters(adjustCJK()); - for (Map.Entry entry : subcommands().entrySet()) { + for (Map.Entry entry : subcommands.entrySet()) { Help help = entry.getValue(); UsageMessageSpec usage = help.commandSpec().usageMessage(); String header = !empty(usage.header()) ? usage.header()[0] : (!empty(usage.description()) ? usage.description()[0] : ""); - Text[] lines = this.colorScheme.text(format(header)).splitLines(); + Text[] lines = colorScheme().text(format(header)).splitLines(); for (int i = 0; i < lines.length; i++) { textTable.addRowValues(i == 0 ? help.commandNamesText(", ") : Ansi.EMPTY_TEXT, lines[i]); } @@ -14816,13 +14903,50 @@ private static String stringOf(char chr, int length) { /** Returns a {@code Layout} instance configured with the user preferences captured in this Help instance. * @return a Layout */ public Layout createDefaultLayout() { - return createLayout(calcLongOptionColumnWidth()); + return createDefaultLayout(options(), positionalParameters(), colorScheme()); + } + /** Returns a {@code Layout} instance configured with the user preferences captured in this Help instance. + * @param options used to calculate the long options column width in the layout + * @param positionals used to calculate the long options column width in the layout + * @param aColorScheme used in the layout to create {@linkplain Text} values + * @return a Layout with the default columns + * @since 4.4 */ + public Layout createDefaultLayout(List options, List positionals, ColorScheme aColorScheme) { + return createLayout(calcLongOptionColumnWidth(options, positionals, aColorScheme), aColorScheme); } - private Layout createLayout(int longOptionsColumnWidth) { - TextTable tt = TextTable.forDefaultColumns(colorScheme, longOptionsColumnWidth, width()); + private Layout createLayout(int longOptionsColumnWidth, ColorScheme aColorScheme) { + TextTable tt = TextTable.forDefaultColumns(aColorScheme, longOptionsColumnWidth, width()); tt.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); - return new Layout(colorScheme, tt, createDefaultOptionRenderer(), createDefaultParameterRenderer()); + return new Layout(aColorScheme, tt, createDefaultOptionRenderer(), createDefaultParameterRenderer()); + } + + private int calcLongOptionColumnWidth(List options, List positionalParamSpecs, ColorScheme aColorScheme) { + int max = 0; + IOptionRenderer optionRenderer = new DefaultOptionRenderer(false, " "); + boolean cjk = commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters(); + int longOptionsColWidth = commandSpec.usageMessage().longOptionsMaxWidth() + 1; // add 1 space for indentation + for (OptionSpec option : options) { + if (option.hidden()) { continue; } + Text[][] values = optionRenderer.render(option, parameterLabelRenderer(), aColorScheme); + int len = cjk ? values[0][3].getCJKAdjustedLength() : values[0][3].length; + if (len < longOptionsColWidth) { max = Math.max(max, len); } + } + List positionals = new ArrayList(positionalParamSpecs); // iterate in declaration order + if (hasAtFileParameter()) { + positionals.add(0, AT_FILE_POSITIONAL_PARAM); + AT_FILE_POSITIONAL_PARAM.messages(commandSpec.usageMessage().messages()); + } + //IParameterRenderer paramRenderer = new DefaultParameterRenderer(false, " "); + for (PositionalParamSpec positional : positionals) { + if (positional.hidden()) { continue; } + //Text[][] values = paramRenderer.render(positional, parameterLabelRenderer(), colorScheme); // values[0][3]; // + Text label = parameterLabelRenderer().renderParameterLabel(positional, aColorScheme.ansi(), aColorScheme.parameterStyles); + int len = cjk ? label.getCJKAdjustedLength() : label.length; + if (len < longOptionsColWidth) { max = Math.max(max, len); } + } + + return max + 3; } /** Returns a new default OptionRenderer which converts {@link OptionSpec Options} to five columns of text to match @@ -15275,6 +15399,14 @@ public void addOptions(List options, IParamLabelRenderer paramLabelR } } } + /** Calls {@link #addOption(CommandLine.Model.OptionSpec, CommandLine.Help.IParamLabelRenderer)} for all Options in the specified list. + * @param options options to add usage descriptions for; + * it is the responsibility of the caller to exclude options that should not be shown + * @param paramLabelRenderer object that knows how to render option parameters + * @since 4.4 */ + public void addAllOptions(List options, IParamLabelRenderer paramLabelRenderer) { + for (OptionSpec option : options) { addOption(option, paramLabelRenderer); } + } /** * Delegates to the {@link #optionRenderer option renderer} of this layout to obtain * text values for the specified {@link OptionSpec}, and then calls the {@link #layout(CommandLine.Model.ArgSpec, CommandLine.Help.Ansi.Text[][])} @@ -15297,6 +15429,14 @@ public void addPositionalParameters(List params, IParamLabe } } } + /** Calls {@link #addPositionalParameter(CommandLine.Model.PositionalParamSpec, CommandLine.Help.IParamLabelRenderer)} for all positional parameters in the specified list. + * @param params positional parameters to add usage descriptions for; + * it is the responsibility of the caller to exclude positional parameters that should not be shown + * @param paramLabelRenderer knows how to render option parameters + * @since 4.4 */ + public void addAllPositionalParameters(List params, IParamLabelRenderer paramLabelRenderer) { + for (PositionalParamSpec param : params) { addPositionalParameter(param, paramLabelRenderer); } + } /** * Delegates to the {@link #parameterRenderer parameter renderer} of this layout * to obtain text values for the specified {@linkplain PositionalParamSpec positional parameter}, and then calls @@ -15735,6 +15875,14 @@ public Column(int width, int indent, Overflow overflow) { this.indent = indent; this.overflow = Assert.notNull(overflow, "overflow"); } + public boolean equals(Object obj) { + return obj instanceof Column + && ((Column) obj).width == width + && ((Column) obj).indent == indent + && ((Column) obj).overflow == overflow; + } + public int hashCode() { return 17 * width + 37 * indent + 37 * overflow.hashCode();} + public String toString() { return String.format("Column[width=%d, indent=%d, overflow=%s]", width, indent, overflow); } } /** All usage help message are generated with a color scheme that assigns certain styles and colors to common diff --git a/src/test/java/picocli/ArgGroupTest.java b/src/test/java/picocli/ArgGroupTest.java index 9010f36bc..bb46191dd 100644 --- a/src/test/java/picocli/ArgGroupTest.java +++ b/src/test/java/picocli/ArgGroupTest.java @@ -3411,12 +3411,12 @@ public void testIssue988OptionGroupSectionsShouldIncludeSubgroupOptions() { " -r, --includetree=DIR... Include only the specified CTrees.%n" + " -R, --excludetree=DIR... Exclude the specified CTrees.%n" + "CTree Options:%n" + - " -t, --ctree=DIR The CTree (directory) to process. The cTree name%n" + - " is the basename of the file.%n" + " -b, --includebase=PATH... Include child files of cTree (only works with%n" + " --ctree).%n" + " -B, --excludebase=PATH... Exclude child files of cTree (only works with%n" + " --ctree).%n" + + " -t, --ctree=DIR The CTree (directory) to process. The cTree name%n" + + " is the basename of the file.%n" + "General Options:%n" + " -i, --input=FILE Input filename (no defaults)%n" + " -n, --inputname=PATH User's basename for input files (e.g.%n" + @@ -3597,19 +3597,19 @@ class Issue1027 { } static class InnerPositional1027 { - @Parameters(index = "0") String param0; - @Parameters(index = "1") String param1; + @Parameters(index = "0") String param00; + @Parameters(index = "1") String param01; public InnerPositional1027() {} - public InnerPositional1027(String param0, String param1) { - this.param0 = param0; - this.param1 = param1; + public InnerPositional1027(String param00, String param01) { + this.param00 = param00; + this.param01 = param01; } public boolean equals(Object obj) { if (!(obj instanceof InnerPositional1027)) { return false; } InnerPositional1027 other = (InnerPositional1027) obj; - return TestUtil.equals(this.param0, other.param0) - && TestUtil.equals(this.param1, other.param1); + return TestUtil.equals(this.param00, other.param00) + && TestUtil.equals(this.param01, other.param01); } } static class Inner1027 { @@ -3746,4 +3746,68 @@ public void testIssue1065ExclusiveGroupNoSplitDuplicateSynopsisVariant() { String actual = new CommandLine(new Issue1065ExclusiveGroupNoSplit()).getUsageMessage(Help.Ansi.OFF); assertEquals(expected, actual); } + + @Test + public void testAllOptionsNested() { + class Nested { + @ArgGroup(exclusive = false, multiplicity = "0..*") + List outers; + } + + List argGroupSpecs = new CommandLine(new Nested()).getCommandSpec().argGroups(); + assertEquals(1, argGroupSpecs.size()); + ArgGroupSpec group = argGroupSpecs.get(0); + List options = group.options(); + assertEquals(1, options.size()); + assertEquals("-x", options.get(0).shortestName()); + + List allOptions = group.allOptionsNested(); + assertEquals(2, allOptions.size()); + assertEquals("-x", allOptions.get(0).shortestName()); + assertEquals("-y", allOptions.get(1).shortestName()); + } + + @Test + public void testAllOptionsNested2() { + List argGroupSpecs = new CommandLine(new Issue988()).getCommandSpec().argGroups(); + assertEquals(2, argGroupSpecs.size()); + ArgGroupSpec projectOrTreeOptionsGroup = argGroupSpecs.get(0); + List options = projectOrTreeOptionsGroup.options(); + assertEquals(0, options.size()); + + List allOptions = projectOrTreeOptionsGroup.allOptionsNested(); + assertEquals(6, allOptions.size()); + assertEquals("--cproject", allOptions.get(0).longestName()); + assertEquals("--includetree", allOptions.get(1).longestName()); + assertEquals("--excludetree", allOptions.get(2).longestName()); + assertEquals("--ctree", allOptions.get(3).longestName()); + assertEquals("--includebase", allOptions.get(4).longestName()); + assertEquals("--excludebase", allOptions.get(5).longestName()); + + ArgGroupSpec generalOptionsGroup = argGroupSpecs.get(1); + assertEquals(2, generalOptionsGroup.options().size()); + assertEquals(2, generalOptionsGroup.allOptionsNested().size()); + } + + @Test + public void testAllPositionalParametersNested() { + class Nested { + @ArgGroup(exclusive = false, multiplicity = "0..*") + List outers; + } + + List argGroupSpecs = new CommandLine(new Nested()).getCommandSpec().argGroups(); + assertEquals(1, argGroupSpecs.size()); + ArgGroupSpec group = argGroupSpecs.get(0); + List positionals = group.positionalParameters(); + assertEquals(2, positionals.size()); + assertEquals("", positionals.get(0).paramLabel()); + + List allPositionals = group.allPositionalParametersNested(); + assertEquals(4, allPositionals.size()); + assertEquals("", allPositionals.get(0).paramLabel()); + assertEquals("", allPositionals.get(1).paramLabel()); + assertEquals("", allPositionals.get(2).paramLabel()); + assertEquals("", allPositionals.get(3).paramLabel()); + } } diff --git a/src/test/java/picocli/HelpSubCommandTest.java b/src/test/java/picocli/HelpSubCommandTest.java index f4deb4e1a..e8983f0a9 100644 --- a/src/test/java/picocli/HelpSubCommandTest.java +++ b/src/test/java/picocli/HelpSubCommandTest.java @@ -401,4 +401,19 @@ public void testUsage_NoHeaderIfAllSubcommandHidden() { "Usage: app [COMMAND]%n"); assertEquals(expected, baos.toString()); } + + @Test + public void testHelp_allSubcommands() { + @Command(name = "foo", description = "This is a visible subcommand") class Foo { } + @Command(name = "bar", description = "This is a hidden subcommand", hidden = true) class Bar { } + @Command(name = "app", subcommands = {Foo.class, Bar.class}) class App { } + + CommandLine app = new CommandLine(new App(), new InnerClassFactory(this)); + Help help = app.getHelp(); + assertEquals(2, help.allSubcommands().size()); + assertEquals(new HashSet(Arrays.asList("foo", "bar")), help.allSubcommands().keySet()); + + assertEquals(1, help.subcommands().size()); + assertEquals(new HashSet(Arrays.asList("foo")), help.subcommands().keySet()); + } } \ No newline at end of file diff --git a/src/test/java/picocli/HelpTest.java b/src/test/java/picocli/HelpTest.java index a6faff0db..a6ad15951 100644 --- a/src/test/java/picocli/HelpTest.java +++ b/src/test/java/picocli/HelpTest.java @@ -22,8 +22,8 @@ import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.contrib.java.lang.system.SystemErrRule; import org.junit.contrib.java.lang.system.SystemOutRule; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; -import picocli.CommandLine.ExecutionException; import picocli.CommandLine.Help; import picocli.CommandLine.Help.Ansi.IStyle; import picocli.CommandLine.Help.Ansi.Style; @@ -51,7 +51,6 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -75,6 +74,7 @@ import static picocli.CommandLine.Help.Visibility.ALWAYS; import static picocli.CommandLine.Help.Visibility.NEVER; import static picocli.CommandLine.Help.Visibility.ON_DEMAND; +import static picocli.CommandLine.ScopeType.INHERIT; import static picocli.TestUtil.textArray; import static picocli.TestUtil.usageString; import static picocli.TestUtil.options; @@ -3459,7 +3459,9 @@ public void testDetermineTerminalWidth() throws NoSuchMethodException, Invocatio TestUtil.setTraceLevel("WARN"); assertTrue(systemErrRule.getLog(), systemErrRule.getLog().startsWith("[picocli DEBUG] getTerminalWidth() executing command [")); - assertTrue(systemErrRule.getLog(), systemErrRule.getLog().contains("[picocli DEBUG] getTerminalWidth() parsing output: ")); + //assertTrue(systemErrRule.getLog(), + // systemErrRule.getLog().contains("[picocli DEBUG] getTerminalWidth() parsing output: ") + // || systemErrRule.getLog().contains("[picocli DEBUG] getTerminalWidth() ERROR: java.lang.ClassNotFoundException: java.lang.ProcessBuilder$Redirect")); assertTrue(systemErrRule.getLog(), systemErrRule.getLog().contains("[picocli DEBUG] getTerminalWidth() returning: ")); //assertEquals(-1, width); } @@ -4349,4 +4351,532 @@ class A { } String expected = String.format(""); assertEquals(expected, actual); } + + @Test //#1081 + public void testHelpConstructorShouldNotCallAddAllSubcommandsMethod() { + CommandLine cmd = new CommandLine(CommandSpec.create()) + .setHelpFactory(new IHelpFactory() { + public Help create(CommandSpec commandSpec, ColorScheme colorScheme) { + return new Help(commandSpec, colorScheme) { + @Override + public Map allSubcommands() { + throw new IllegalStateException("surprise"); + } + }; + } + }); + String actual = cmd.getUsageMessage(); + assertEquals(String.format("Usage:
%n"), actual); + } + + @Test //#1081 + public void testHelpConstructorShouldNotCallCreateDefaultParameterRenderer() { + CommandLine cmd = new CommandLine(CommandSpec.create()) + .setHelpFactory(new IHelpFactory() { + public Help create(CommandSpec commandSpec, ColorScheme colorScheme) { + return new Help(commandSpec, colorScheme) { + @Override + public IParamLabelRenderer createDefaultParamLabelRenderer() { + throw new IllegalStateException("Surprise!"); + } + }; + } + }); + String actual = cmd.getUsageMessage(); + assertEquals(String.format("Usage:
%n"), actual); + } + + @Test + public void testHelp_createDefaultOptionSort_returnsNullIfSortIsFalseAndAllOptionsHaveDefaultOrder() { + @Command(sortOptions = false) + class App { + @Option(names = "-a", scope = INHERIT, description = "a option") boolean a; + @Option(names = "-b", scope = INHERIT, description = "b option") boolean b; + @Option(names = "-c", scope = INHERIT, description = "c option") boolean c; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + assertNull(help.createDefaultOptionSort()); + } + + @Test + public void testHelp_createDefaultOptionSort_returnsComparatorByOrderIfSortIsFalseAndAnyOptionsHasNonDefaultOrder() { + @Command(sortOptions = false) + class App { + @Option(names = "-a", scope = INHERIT, description = "a option", order = 123) boolean a; + @Option(names = "-b", scope = INHERIT, description = "b option") boolean b; + @Option(names = "-c", scope = INHERIT, description = "c option", order = 111) boolean c; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Comparator optionSort = help.createDefaultOptionSort(); + assertNotNull(optionSort); + List options = new ArrayList(spec.options()); + assertEquals(Arrays.asList(spec.findOption('a'), + spec.findOption('b'), + spec.findOption('c') + ), options); + Collections.sort(options, optionSort); + assertEquals(Arrays.asList(spec.findOption('b'), + spec.findOption('c'), + spec.findOption('a') + ), options); + } + + @Test + public void testHelp_createDefaultOptionSort_returnsComparatorByShortNameIfSortIsTrue() { + @Command(sortOptions = true) + class App { + @Option(names = "-a", scope = INHERIT, description = "a option") boolean a; + @Option(names = "-e", scope = INHERIT, description = "e option") boolean e; + @Option(names = "-c", scope = INHERIT, description = "c option") boolean c; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Comparator optionSort = help.createDefaultOptionSort(); + assertNotNull(optionSort); + List options = new ArrayList(spec.options()); + assertEquals(Arrays.asList(spec.findOption('a'), + spec.findOption('e'), + spec.findOption('c') + ), options); + Collections.sort(options, optionSort); + assertEquals(Arrays.asList(spec.findOption('a'), + spec.findOption('c'), + spec.findOption('e') + ), options); + } + + @Test + public void testHelp_createDefaultOptionSort_returnsComparatorByShortNameIfSortIsTrueIgnoringOptionOrder() { + @Command(sortOptions = true) + class App { + @Option(names = "-a", scope = INHERIT, description = "a option", order = 123) boolean a; + @Option(names = "-e", scope = INHERIT, description = "e option") boolean e; + @Option(names = "-c", scope = INHERIT, description = "c option", order = 111) boolean c; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Comparator optionSort = help.createDefaultOptionSort(); + assertNotNull(optionSort); + List options = new ArrayList(spec.options()); + assertEquals(Arrays.asList(spec.findOption('a'), + spec.findOption('e'), + spec.findOption('c') + ), options); + Collections.sort(options, optionSort); + assertEquals(Arrays.asList(spec.findOption('a'), + spec.findOption('c'), + spec.findOption('e') + ), options); + } + + @Test //#1052 + public void testCustomizeCommandListViaRenderer() { + @Command + class MyApp { + @Command(description = "subcommand foo") + void foo() {} + + @Command(description = "subcommand bar", hidden = true) + void bar() {} + + @Command(description = "subcommand baz", hidden = true) + void baz() {} + + @Command(description = "subcommand xxx") + void xxx() {} + + @Command(description = "subcommand yyy") + void yyy() {} + + @Command(description = "subcommand zzz") + void zzz() {} + } + + IHelpSectionRenderer renderer = new IHelpSectionRenderer() { + public String render(Help help) { + Map mySubcommands = createCustomCommandList(help); + return help.commandList(mySubcommands); + } + + private Map createCustomCommandList(Help help) { + Map result = new LinkedHashMap(); + // register commands in the specific order we want to see them in the usage help + for (String included : new String[]{"baz", "foo", "zzz", "yyy"}) { + result.put(included, help.allSubcommands().get(included)); + } + return result; + } + }; + CommandLine cmd = new CommandLine(new MyApp()); + cmd.getHelpSectionMap().put(UsageMessageSpec.SECTION_KEY_COMMAND_LIST, renderer); + assertEquals(String.format("" + + "Usage:
[COMMAND]%n" + + "Commands:%n" + + " baz subcommand baz%n" + + " foo subcommand foo%n" + + " zzz subcommand zzz%n" + + " yyy subcommand yyy%n"), cmd.getUsageMessage()); + } + + @Test //#1052 + public void testCustomizeOptionListViaRenderer() { + @Command + class App { + @Option(names = "-a", scope = INHERIT, description = "a option") boolean a; + @Option(names = "-b", scope = INHERIT, description = "b option") boolean b; + @Option(names = "-c", scope = INHERIT, description = "c option") boolean c; + + @Command void sub() {} + } + + IHelpSectionRenderer renderer = new IHelpSectionRenderer() { + public String render(Help help) { + List options = new ArrayList(help.commandSpec().options()); + options.remove(help.commandSpec().findOption("-b")); + return help.optionListExcludingGroups(options); + } + }; + + CommandLine cmd = new CommandLine(new App()); + cmd.getHelpSectionMap().put(UsageMessageSpec.SECTION_KEY_OPTION_LIST, renderer); + assertEquals(String.format("" + + "Usage:
[-ac] [COMMAND]%n" + // TODO synopsis abc + " -a a option%n" + + " -c c option%n" + + "Commands:%n" + + " sub%n"), cmd.getUsageMessage()); + } + + @Test //#1052 + public void testCustomizeOptionListViaInheritance() { + @Command + class App { + @Option(names = "-a", scope = INHERIT, description = "a option") boolean a; + @Option(names = "-b", scope = INHERIT, description = "b option") boolean b; + @Option(names = "-c", scope = INHERIT, description = "c option") boolean c; + + @Command(description = "This is the `sub` subcommand...") void sub() {} + } + + CommandLine cmd = new CommandLine(new App()) + .setHelpFactory(new IHelpFactory() { + public Help create(CommandSpec commandSpec, ColorScheme colorScheme) { + return new Help(commandSpec, colorScheme) { + @Override + public String optionListExcludingGroups(List options) { + List shown = new ArrayList(); + for (OptionSpec option : options) { + if (!option.inherited() || option.shortestName().equals("-b")) { + shown.add(option); + } + } + return super.optionListExcludingGroups(shown); + } + }; + } + }); + String actual = cmd.getSubcommands().get("sub").getUsageMessage(); + assertEquals(String.format("" + + "Usage:
sub [-b]%n" + // TODO synopsis -abc + "This is the `sub` subcommand...%n" + + " -b b option%n"), actual); + } + + @Test + public void testHelp_optionList_noArgs_includesArgGroups() { + @Command + class App { + @Option(names = "-x", description = "option x") boolean x; + @Option(names = "-y", description = "option y") boolean y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + Help help = new Help(CommandSpec.forAnnotatedObject(new App())); + assertEquals(String.format("" + + " -x option x%n" + + " -y option y%n" + + "Usage Options%n" + + " 0BLAH first parameter%n" + + " 1PARAMETER-with-a-name-so-long-that-it-runs-into-the-descriptions-column%n" + + " 2nd parameter%n" + + " remaining remaining parameters%n" + + " all all parameters%n" + + " -a boolean option with short name only%n" + + " -b=INT short option with a parameter%n" + + " -c, --c-option boolean option with short and long name%n" + + " -d, --d-option=FILE option with parameter and short and long name%n" + + " --e-option boolean option with only a long name%n" + + " --f-option=STRING option with parameter and only a long name%n" + + " -g, --g-option-with-a-name-so-long-that-it-runs-into-the-descriptions-column%n" + + " boolean option with short and long name%n" + + ""), help.optionList()); + } + + @Test + public void testHelp_optionList_list_excludesArgGroups() { + @Command + class App { + @Option(names = "-x", description = "option x") boolean x; + @Option(names = "-y", description = "option y") boolean y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + String actual = help.optionListExcludingGroups(Arrays.asList(spec.findOption('x'), spec.findOption('y'))); + assertEquals(String.format("" + + " -x option x%n" + + " -y option y%n"), actual); + } + + @Test + public void testHelp_optionList_list__layout_comparator_labelrenderer_excludesArgGroups() { + @Command + class App { + @Option(names = "-z", description = "option z") boolean z; + @Option(names = "-y", description = "option y") boolean y; + @Option(names = "-x", description = "option x") boolean x; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + + String actual = help.optionListExcludingGroups(Arrays.asList( + spec.findOption('x'), + spec.findOption('y')), + help.createDefaultLayout(), help.createDefaultOptionSort(), help.createDefaultParamLabelRenderer()); + assertEquals("Only shows specified options", String.format("" + + " -x option x%n" + + " -y option y%n" + + ""), actual); + } + + @Test + public void testHelp_argumentGroupList() { + @Command + class App { + @Option(names = "-x", description = "option x") boolean x; + @Option(names = "-y", description = "option y") boolean y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + Help help = new Help(CommandSpec.forAnnotatedObject(new App())); + assertEquals(String.format("" + + "Usage Options%n" + + " 0BLAH first parameter%n" + + " 1PARAMETER-with-a-name-so-long-that-it-runs-into-the-descriptions-column%n" + + " 2nd parameter%n" + + " remaining remaining parameters%n" + + " all all parameters%n" + + " -a boolean option with short name only%n" + + " -b=INT short option with a parameter%n" + + " -c, --c-option boolean option with short and long name%n" + + " -d, --d-option=FILE option with parameter and short and long name%n" + + " --e-option boolean option with only a long name%n" + + " --f-option=STRING option with parameter and only a long name%n" + + " -g, --g-option-with-a-name-so-long-that-it-runs-into-the-descriptions-column%n" + + " boolean option with short and long name%n" + + ""), help.optionListGroupSections()); + } + + static class Section { + static class G0 { + @Option(names = "-0", description = "option -0 in G0") boolean zero; + } + static class G1 { + @Option(names = "-a", description = "option -a in G1") boolean a; + } + static class G2 { + @Option(names = "-b", description = "option -b in G2") boolean b; + } + static class G3 { + @Option(names = "-c", description = "option -c in G3") boolean c; + + @ArgGroup(heading = "INNER GROUP%n") + G1 group; + + @ArgGroup + G2 group2; + } + @ArgGroup(heading = "GROUP%n") + G3 group; + } + @Test + public void testHelp_optionSectionGroups_returnsGroupsWithHeaderIncludingSubgroups() { + CommandSpec spec = CommandSpec.forAnnotatedObject(new Section()); + Help help = new Help(spec); + List argGroups = help.optionSectionGroups(); + assertEquals(2, argGroups.size()); + assertEquals("INNER GROUP%n", argGroups.get(0).heading()); + assertEquals("GROUP%n", argGroups.get(1).heading()); + } + + @Test + public void testHelp_parameterList_noargs_excludesHiddenAndGroupParams() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + String actual = help.parameterList(); + assertEquals(String.format("" + + " VISIBLE visible positional%n" + + ""), actual); + } + + @Test + public void testHelp_parameterList_list_showsAllSpecifiedParams() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + List params = new ArrayList(); + for (PositionalParamSpec param : spec.positionalParameters()) { + if (Arrays.asList("0BLAH", "HIDDEN", "VISIBLE").contains(param.paramLabel())) { + params.add(param); + } + } + String actual = help.parameterList(params); + assertEquals(String.format("" + + " 0BLAH first parameter%n" + + " HIDDEN hidden positional%n" + + " VISIBLE visible positional%n" + + ""), actual); + } + + @Test + public void testHelp_parameterList_layout_paramRenderer_excludesHiddenAndGroupParams() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + String actual = help.parameterList(help.createDefaultLayout(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " VISIBLE visible positional%n" + + ""), actual); + } + + @Test + public void testHelp_parameterList_list_layout_paramRenderer_showsAllSpecifiedParams() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + @ArgGroup(heading = "Usage Options%n") + UsageDemo usageDemo; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + List params = new ArrayList(); + for (PositionalParamSpec param : spec.positionalParameters()) { + if (Arrays.asList("0BLAH", "HIDDEN", "VISIBLE").contains(param.paramLabel())) { + params.add(param); + } + } + String actual = help.parameterList(params, help.createDefaultLayout(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " 0BLAH first parameter%n" + + " HIDDEN hidden positional%n" + + " VISIBLE visible positional%n" + + ""), actual); + } + + @Test + public void testHelp_createDefaultLayout_usesSpecifiedOptionsAndParamsToCalculateLongOptionColumnWidth() { + List options = Arrays.asList(OptionSpec.builder("-a", "--long-option-name").build()); + List positionals = Arrays.asList(PositionalParamSpec.builder().paramLabel("LONG_PARAM_LABEL").build()); + Help help = new Help(CommandSpec.create()); + ColorScheme colorScheme = new ColorScheme.Builder().build(); + Help.Layout layout = help.createDefaultLayout(options, positionals, colorScheme); + assertSame(colorScheme, layout.colorScheme); + assertEquals(5, layout.table.columns().length); + int longOptionCol = Math.max("--long-option-name".length(), "LONG_PARAM_LABEL".length()) + 1 + 2; // 1 indent, 2 spacing to next col + Help.Column[] expected = new Help.Column[] { + new Help.Column(2, 0, Help.Column.Overflow.TRUNCATE), + new Help.Column(2, 0, Help.Column.Overflow.SPAN), + new Help.Column(1, 0, Help.Column.Overflow.TRUNCATE), + new Help.Column(longOptionCol, 1, Help.Column.Overflow.SPAN), + new Help.Column(80 - (2 + 2 + 1 + longOptionCol), 1, Help.Column.Overflow.WRAP), + }; + assertArrayEquals(expected, layout.table.columns()); + } + + @Test + public void testHelpLayout_addOptions_excludesHiddenOptions() { + @Command + class App { + @Option(names = "-x", description = "option x", hidden = true) boolean x; + @Option(names = "-y", description = "option y") boolean y; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Help.Layout layout = help.createDefaultLayout(); + layout.addOptions(spec.options(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " -y option y%n"), layout.toString()); + } + + @Test + public void testHelpLayout_addAllOptions_includesHiddenOptions() { + @Command + class App { + @Option(names = "-x", description = "option x", hidden = true) boolean x; + @Option(names = "-y", description = "option y") boolean y; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Help.Layout layout = help.createDefaultLayout(); + layout.addAllOptions(spec.options(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " -x option x%n" + + " -y option y%n"), layout.toString()); + } + + @Test + public void testHelpLayout_addPositionalParameters_includesHiddenOptions() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Help.Layout layout = help.createDefaultLayout(); + layout.addPositionalParameters(spec.positionalParameters(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " VISIBLE visible positional%n"), layout.toString()); + } + + @Test + public void testHelpLayout_addAllPositionalParameters_includesHiddenOptions() { + @Command + class App { + @Parameters(paramLabel = "HIDDEN", description = "hidden positional", hidden = true) String x; + @Parameters(paramLabel = "VISIBLE", description = "visible positional") String y; + } + CommandSpec spec = CommandSpec.forAnnotatedObject(new App()); + Help help = new Help(spec); + Help.Layout layout = help.createDefaultLayout(); + layout.addAllPositionalParameters(spec.positionalParameters(), help.parameterLabelRenderer()); + assertEquals(String.format("" + + " HIDDEN hidden positional%n" + + " VISIBLE visible positional%n"), layout.toString()); + } }