maedhroz commented on code in PR #2497: URL: https://github.com/apache/cassandra/pull/2497#discussion_r2231993239
########## src/java/org/apache/cassandra/tools/nodetool/layout/CassandraCliHelpLayout.java: ########## @@ -0,0 +1,786 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.tools.nodetool.layout; + +import org.apache.cassandra.tools.nodetool.CommandUtils; +import org.apache.cassandra.tools.nodetool.JmxConnect; +import org.apache.cassandra.tools.nodetool.TopLevelCommand; +import org.apache.cassandra.utils.Pair; +import org.apache.commons.lang3.StringUtils; +import picocli.CommandLine; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.cassandra.tools.nodetool.CommandUtils.findCassandraBackwardCompatibleArgument; +import static org.apache.cassandra.tools.nodetool.CommandUtils.sortShortestFirst; +import static org.apache.commons.lang3.ArrayUtils.isEmpty; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_END_OF_OPTIONS; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING; + +/** + * Help factory for the Cassandra nodetool to generate the help output in the format + * of the airline help output, which is used as the default layout for the Cassandra nodetool. + * <p> + * Note, that JMX connect options are always shown in the help output and are not hidden. The + * {@link JmxConnect} class is used to connect to a C* node via JMX, but the opttions are not + * part of the command hierarchy to allow reusage of the commands in other contexts. + */ +public class CassandraCliHelpLayout extends CommandLine.Help +{ + // The default width for the usage help output to match the width of + // the airline help output and minimize the divergence of layouts. + public static final int DEFAULT_USAGE_HELP_WIDTH = 88; + private static final int TOP_LEVEL_USAGE_HELP_WIDTH = 999; + private static final String DESCRIPTION_HEADING = "NAME%n"; + private static final String SYNOPSIS_HEADING = "SYNOPSIS%n"; + private static final String OPTIONS_HEADING = "OPTIONS%n"; + private static final String COMMANDS_HEADING = "COMMANDS%n"; + private static final String FOOTER_HEADING = "%n"; + private static final String SINGLE_SPACE = " "; + private static final int DESCRIPTION_INDENT = 4; + public static final int COLUMN_INDENT = 8; + public static final int SUBCOMMANDS_INDENT = 4; + public static final int SUBCOMMANDS_DESCRIPTION_INDENT_TOP_LEVEL = 3; + private static final CommandLine.Model.OptionSpec CASSANDRA_END_OF_OPTIONS_OPTION = + CommandLine.Model.OptionSpec.builder("--") + .description("This option can be used to separate command-line options from the " + + "list of argument, (useful when arguments might be mistaken for " + + "command-line options") + .arity("0") + .build(); + public static final String TOP_LEVEL_SYNOPSIS_LIST_PREFIX = "usage:"; + public static final String TOP_LEVEL_COMMAND_HEADING = "The most commonly used nodetool commands are:"; + public static final String USAGE_HELP_FOOTER = "See 'nodetool help <command>' for more information on a specific command."; + public static final String SYNOPSIS_SUBCOMMANDS_LABEL = "<command> [<args>]"; + public static final String SUBCOMMAND_OPTION_TEMPLATE = "With %s option, %s"; + public static final String SUBCOMMAND_SUBHEADER = "With no arguments, Display help information"; + private static final String[] EMPTY_FOOTER = new String[0]; + + public CassandraCliHelpLayout(CommandLine.Model.CommandSpec spec, ColorScheme scheme) + { + super(spec, scheme); + } + + @Override + public String descriptionHeading(Object... params) + { + return createHeading(DESCRIPTION_HEADING, params); + } + + /** + * @param params Arguments referenced by the format specifiers in the header strings + * @return the header string. + */ + @Override + public String description(Object... params) + { + CommandLine.Model.CommandSpec spec = commandSpec(); + String fullName = spec.qualifiedName(); + + TextTable table = TextTable.forColumns(colorScheme(), + new Column(spec.usageMessage().width() - COLUMN_INDENT, COLUMN_INDENT, + Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(spec.usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = 0; + + table.addRowValues(colorScheme().commandText(fullName) + .concat(" - ") + .concat(colorScheme().text(String.join(" ", spec.usageMessage().description())))); + table.addRowValues(Ansi.OFF.new Text("", colorScheme())); + return table.toString(new StringBuilder()).toString(); + } + + @Override + public String synopsisHeading(Object... params) + { + return createHeading(SYNOPSIS_HEADING, params); + } + + /** + * This method is overridden to provide a detailed synopsis for the command and its subcommands. + * <pre> + * {@code + * SYNOPSIS + * nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)] + * [(-pp | --print-port)] [(-pw <password> | --password <password>)] + * [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)] + * [(-u <username> | --username <username>)] bootstrap <command> + * [<args>] + * } + * </pre> + * + * @param synopsisHeadingLength the length of the synopsis heading that will be displayed on the same line + * @return The synopsis string. + */ + @Override + public String synopsis(int synopsisHeadingLength) + { + return printDetailedSynopsis("", COLUMN_INDENT, true); + } + + private String printDetailedSynopsis(String synopsisPrefix, int columnIndent, boolean showEndOfOptionsDelimiter) + { + StringBuilder top = new StringBuilder(printDetailedSynopsis(commandSpec(), synopsisPrefix, columnIndent, showEndOfOptionsDelimiter)); + for (CommandLine sub : commandSpec().subcommands().values()) + top.append(printDetailedSynopsis(sub.getCommandSpec(), synopsisPrefix, columnIndent, showEndOfOptionsDelimiter)); + return top.toString(); + } + + private String printDetailedSynopsis(CommandLine.Model.CommandSpec commandSpec, + String synopsisPrefix, + int columnIndent, + boolean showEndOfOptionsDelimiter) + { + // Cassandra uses end of options delimiter in usage help. + commandSpec.usageMessage().showEndOfOptionsDelimiterInUsageHelp(showEndOfOptionsDelimiter); + + ColorScheme colorScheme = colorScheme(); + + List<CommandLine.Model.OptionSpec> parentOptions = parentCommandOptionsWithJmxOptions(commandSpec); + List<CommandLine.Model.OptionSpec> commandOptions = commandSpec.options(); + // Retain only the options that are not part of the command hierarchy (e.g. dynamic options). + Comparator<CommandLine.Model.OptionSpec> comparator = new OptionSpecByNamesComparator(); + parentOptions.removeIf(parentOpt -> + commandOptions.stream().anyMatch(commandOpt -> comparator.compare(parentOpt, commandOpt) == 0)); + + List<Ansi.Text> parentOptionsList = createCassandraSynopsisOptionsText(parentOptions); + List<Ansi.Text> commandOptionsList = createCassandraSynopsisOptionsText(commandOptions); + + Ansi.Text positionalParamText = createCassandraSynopsisPositionalsText(commandSpec, colorScheme); + Ansi.Text endOfOptionsText = positionalParamText.plainString().isEmpty() ? + colorScheme.text("") : + colorScheme.text("[") + .concat(commandSpec.parser().endOfOptionsDelimiter()) + .concat("]"); + + Ansi.Text commandText = ansi().new Text(0); + if (!commandSpec.subcommands().isEmpty()) + commandText = commandText.concat(SYNOPSIS_SUBCOMMANDS_LABEL); + + int usageHelpWidth = commandSpec.usageMessage().width(); + boolean isEmptyParent = commandSpec.root() == commandSpec; + Ansi.Text rootCommandText = colorScheme.commandText(commandSpec.root().name()); + // If the command is the top-level command, use the command name as the root command text. + // Otherwise, use the fully qualified command name without the root command name. + // Example: "nodetool status" -> "status", "nodetool status thrift" -> "status thrift" + Ansi.Text mainCommandText = isEmptyParent ? colorScheme.commandText(commandSpec.name()) : + colorScheme.commandText(commandSpec.qualifiedName().replace(rootCommandText.plainString(), "").trim()); + TextTable textTable = TextTable.forColumns(colorScheme, new Column(usageHelpWidth, columnIndent, Column.Overflow.WRAP)); + textTable.indentWrappedLines = columnIndent; + textTable.setAdjustLineBreaksForWideCJKCharacters(commandSpec.usageMessage().adjustLineBreaksForWideCJKCharacters()); + + // Consider the following example: + // SYNOPSIS + // nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)] + // [(-pw <password> | --password <password>)] + // [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)] + // [(-u <username> | --username <username>)] describecluster + // [(-pp | --print-port)] + new LineBreakingLayout(colorScheme, usageHelpWidth, textTable) + .concatItem(synopsisPrefix.isEmpty() ? rootCommandText : colorScheme.text(synopsisPrefix).concat(" ").concat(rootCommandText)) + // Print "[(-h <host> | --host <host>)] [(-p <port> | --port <port>)]" options related to the parent command. + .concatItems(parentOptionsList) + // Print "describecluster" in the same line as the parent options. + .concatItem(isEmptyParent ? colorScheme.text("") : mainCommandText) + // Print "[(-pp | --print-port)]" options related to the command itself. + .concatItems(commandOptionsList) + .concatItem(endOfOptionsText) + // All other fields added to the synopsis are left-adjusted, so we don't need to add them one by one. + .flush(positionalParamText.concat(commandText)); + + textTable.addEmptyRow(); + return textTable.toString(); + } + + private static Ansi.Text createCassandraSynopsisPositionalsText(CommandLine.Model.CommandSpec spec, + ColorScheme colorScheme) + { + List<CommandLine.Model.PositionalParamSpec> positionals = cassandraPositionals(spec); + + Pair<String, String> commandArgumensSpec = findCassandraBackwardCompatibleArgument(spec.userObject()); + Ansi.Text text = colorScheme.text(""); + // If the command has a backward compatible @CassandraUsage argument, + // use it to generate the synopsis based on the old format. + if (commandArgumensSpec != null) + return colorScheme.parameterText(commandArgumensSpec.left); + + IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create(); + for (CommandLine.Model.PositionalParamSpec positionalParam : positionals) + { + Ansi.Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles()); + text = text.plainString().isEmpty() ? label : text.concat(" ").concat(label); + } + return text; + } + + private static List<CommandLine.Model.OptionSpec> parentCommandOptionsWithJmxOptions(final CommandLine.Model.CommandSpec commandSpec) + { + // If the command is the help local command, no need to show the parent options. + if (commandSpec.helpCommand()) + return Collections.emptyList(); + + List<CommandLine.Model.CommandSpec> hierarhy = new LinkedList<>(); + CommandLine.Model.CommandSpec curr; + CommandLine.Model.CommandSpec command = commandSpec; + while ((curr = command.parent()) != null) + { + hierarhy.add(curr); + command = curr; + } + Collections.reverse(hierarhy); + List<CommandLine.Model.OptionSpec> options = new ArrayList<>(); + for (CommandLine.Model.CommandSpec spec : hierarhy) + { + for (CommandLine.Model.OptionSpec option : spec.options()) + { + // JmxConnect and TopLevelCommand options are always shown in the help output for backwards compatibility. + if (option.userObject() instanceof Field && + (((Field) option.userObject()).getDeclaringClass().equals(JmxConnect.class) || + ((Field) option.userObject()).getDeclaringClass().equals(TopLevelCommand.class))) + options.add(option); + else + { + if (option.hidden() || option.scopeType() == CommandLine.ScopeType.LOCAL) + continue; + options.add(option); + } + } + } + return options; + } + + private List<Ansi.Text> createCassandraSynopsisOptionsText(List<CommandLine.Model.OptionSpec> options) + { + // Cassandra uses alphabetical order for options, ordered by short name. + List<CommandLine.Model.OptionSpec> optionList = new ArrayList<>(options); + optionList.sort(createShortOptionNameComparator()); + List<Ansi.Text> result = new ArrayList<>(); + + ColorScheme colorScheme = colorScheme(); + IParamLabelRenderer parameterLabelRenderer = CassandraStyleParamLabelRender.create(); + + for (CommandLine.Model.OptionSpec option : optionList) + { + if (option.hidden()) + continue; + + Ansi.Text text = ansi().new Text(0); + String[] names = sortShortestFirst(option.names()); + if (names.length == 1) + { + text = text.concat("[").concat(colorScheme.optionText(names[0])) + .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme)) + .concat("]"); + } + else + { + Ansi.Text shortName = colorScheme.optionText(option.shortestName()); + Ansi.Text fullName = colorScheme.optionText(option.longestName()); + boolean isArrayOrCollection = + option.userObject() instanceof Field + && (((Field) option.userObject()).getType().isArray() || + Collection.class.isAssignableFrom(((Field) option.userObject()).getType())); + text = text.concat("[(") + .concat(shortName) + .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme)) + .concat(" | ") + .concat(fullName) + .concat(spacedParamLabel(option, parameterLabelRenderer, colorScheme)) + .concat(isArrayOrCollection ? ")...]" : ")]"); + } + + result.add(text); + } + return result; + } + + @Override + public String optionListHeading(Object... params) + { + return createHeading(OPTIONS_HEADING, params); + } + + /** + * Returns the help for the options of the current command. + * <pre> + * {@code + * OPTIONS + * --help + * Show usage help for the help command and exit. + * + * -- + * This option can be used to separate command-line options from + * the list of argument, (useful when arguments might be mistaken + * for command-line options + * } + * </pre> + * @return The string representation of the options. + */ + @Override + public String optionList() + { + CommandLine.Model.CommandSpec spec = commandSpec(); + List<CommandLine.Model.OptionSpec> commandOptions = spec.options(); + List<CommandLine.Model.OptionSpec> parentOptions = parentCommandOptionsWithJmxOptions(spec); + Comparator<CommandLine.Model.OptionSpec> comparator = new OptionSpecByNamesComparator(); + parentOptions.removeIf(parentOpt -> + commandOptions.stream().anyMatch(commandOpt -> comparator.compare(parentOpt, commandOpt) == 0)); + + List<CommandLine.Model.OptionSpec> uniqueOptions = Stream.concat(parentOptions.stream(), + commandOptions.stream().filter(o -> !o.hidden())) + .distinct() + .sorted(createShortOptionNameComparator()) + .collect(Collectors.toList()); + + Layout layout = cassandraSingleColumnOptionsParametersLayout(); + layout.addAllOptions(uniqueOptions, CassandraStyleParamLabelRender.create()); + return layout.toString(); + } + + @Override + public String endOfOptionsList() + { + List<CommandLine.Model.PositionalParamSpec> positionals = cassandraPositionals(commandSpec()); + if (positionals.isEmpty()) + return ""; + Layout layout = cassandraSingleColumnOptionsParametersLayout(); + layout.addOption(CASSANDRA_END_OF_OPTIONS_OPTION, CassandraStyleParamLabelRender.create()); + return layout.toString(); + } + + private Layout cassandraSingleColumnOptionsParametersLayout() + { + return new Layout(colorScheme(), configureLayoutTextTable(), new CassandraStyleOptionRenderer(), new CassandraStyleParameterRenderer()); + } + + private TextTable configureLayoutTextTable() + { + TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width() - COLUMN_INDENT, + COLUMN_INDENT, Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = DESCRIPTION_INDENT; + return table; + } + + @Override + public String parameterList() + { + Pair<String, String> cassandraArgument = findCassandraBackwardCompatibleArgument(commandSpec().userObject()); + List<CommandLine.Model.PositionalParamSpec> positionalParams = cassandraPositionals(commandSpec()); + TextTable table = configureLayoutTextTable(); + Layout layout = cassandraArgument == null ? + cassandraSingleColumnOptionsParametersLayout() : + new Layout(colorScheme(), + table, + new CassandraStyleOptionRenderer(), + new CassandraStyleParameterRenderer()) + { + // If the command has a backward compatible argument, use it to generate the synopsis + // based on the old format. + @Override + public void layout(CommandLine.Model.ArgSpec argSpec, Ansi.Text[][] cellValues) + { + Ansi.Text descPadding = Ansi.OFF.new Text(leadingSpaces(DESCRIPTION_INDENT), colorScheme); + cellValues[0] = new Ansi.Text[]{ colorScheme.parameterText(cassandraArgument.left) }; + cellValues[1] = new Ansi.Text[]{ descPadding.concat(colorScheme.parameterText(cassandraArgument.right)) }; + cellValues[2] = new Ansi.Text[]{ Ansi.OFF.new Text("", colorScheme) }; + for (Ansi.Text[] oneRow : cellValues) + table.addRowValues(oneRow); + } + + @Override + public void addAllPositionalParameters(List<CommandLine.Model.PositionalParamSpec> params, + IParamLabelRenderer paramLabelRenderer) + { + layout(null, new Ansi.Text[3][]); + } + }; + + layout.addAllPositionalParameters(positionalParams, CassandraStyleParamLabelRender.create()); + table.addEmptyRow(); + return layout.toString(); + } + + @Override + public String commandListHeading(Object... params) + { + if (commandSpec().subcommands().isEmpty()) + return ""; + return createHeading(COMMANDS_HEADING, params); + } + + /** + * Returns the help for the subcommands of the current command. + * <pre> + * {@code + * COMMANDS + * With no arguments, Display help information + * + * resume + * Resume bootstrap streaming + * + * With --force option, Use --force to resume bootstrap regardless of + * cassandra.reset_bootstrap_progress environment variable. WARNING: This + * is potentially dangerous, see CASSANDRA-17679 + * } + * </pre> + * @param subcommands The subcommands of the current command. + * @return The string representation of the subcommands. + */ + @Override + public String commandList(Map<String, CommandLine.Help> subcommands) + { + if (subcommands.isEmpty()) + return ""; + TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width(), + COLUMN_INDENT, Column.Overflow.WRAP)); + table.indentWrappedLines = SUBCOMMANDS_INDENT; + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.addRowValues(colorScheme().parameterText(SUBCOMMAND_SUBHEADER)); + table.addEmptyRow(); + + for (Map.Entry<String, CommandLine.Help> entry : subcommands.entrySet()) + { + CommandLine.Help help = entry.getValue(); + CommandLine.Model.UsageMessageSpec usage = help.commandSpec().usageMessage(); + String header = isEmpty(usage.header()) ? (isEmpty(usage.description()) ? "" : usage.description()[0]) : usage.header()[0]; + table.addRowValues(colorScheme().commandText(entry.getKey())); + + Ansi.Text leadingSpaces = Ansi.OFF.new Text(leadingSpaces(SUBCOMMANDS_INDENT), colorScheme()); + table.addRowValues(leadingSpaces.concat(colorScheme().text(header))); + + List<CommandLine.Model.OptionSpec> optionSpecs = help.commandSpec().options(); + for (CommandLine.Model.OptionSpec optionSpec : optionSpecs) + { + if (optionSpec.hidden()) + continue; + table.addEmptyRow(); + + // Print the option description in multiple lines as it is set in the annotation. + for (int i = 0; i < optionSpec.description().length; i++) + { + if (i == 0) + { + table.addRowValues(leadingSpaces.concat( + colorScheme().optionText(String.format(SUBCOMMAND_OPTION_TEMPLATE, + optionSpec.longestName(), + colorScheme().text(optionSpec.description()[i]))))); + } + else + { + table.addRowValues(leadingSpaces.concat(colorScheme().text(optionSpec.description()[i]))); + } + } + } + } + return table.toString(); + } + + @Override + public String footerHeading(Object... params) + { + return createHeading(FOOTER_HEADING, params); + } + + @Override + public String footer(Object... params) + { + String[] footer; + if (commandSpec().parent() == null) + footer = isEmpty(commandSpec().usageMessage().footer()) ? new String[]{ USAGE_HELP_FOOTER } : + commandSpec().usageMessage().footer(); + else + footer = EMPTY_FOOTER; + + TextTable table = TextTable.forColumns(colorScheme(), new Column(commandSpec().usageMessage().width(), 0, Column.Overflow.WRAP)); + table.setAdjustLineBreaksForWideCJKCharacters(commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()); + table.indentWrappedLines = 0; + + for (String summaryLine : footer) + table.addRowValues(String.format(summaryLine, params)); + table.addEmptyRow(); + return table.toString(); + } + + public String topLevelCommandListHeading(Object... params) + { + return createHeading(TOP_LEVEL_COMMAND_HEADING + "%n", params); + } + + public String topLevelSynopsis(Object... params) + { + return printDetailedSynopsis(commandSpec(), TOP_LEVEL_SYNOPSIS_LIST_PREFIX, COLUMN_INDENT, false); + } + + /** + * Returns the help for the top-level command. This differs from the {@link #commandList(Map)} method + * in that it does not include the subcommands. + * <pre> + * {@code + * The most commonly used nodetool commands are: + * abortbootstrap Abort a failed bootstrap + * bootstrap Monitor/manage node's bootstrap process + * cidrfilteringstats Print statistics on CIDR filtering + * clientstats Print information about connected clients + * } + * </pre> + * @param params Arguments referenced by the format specifiers in the header strings. + * @return The top-level subcommands list. + */ + public String topLevelCommandList(Object... params) Review Comment: `params` is unused? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For additional commands, e-mail: pr-h...@cassandra.apache.org