From 35fbde0e70b8a715774aee1f33c032dfd304317d Mon Sep 17 00:00:00 2001 From: Aleksandr Pakhomov Date: Thu, 29 Aug 2024 16:38:30 +0300 Subject: [PATCH] IGNITE-22777 Improve CLI usability (#4280) Not the usage text is more user-friendly --- .../internal/cli/commands/BaseCommand.java | 29 ++++++++++++- .../cli/commands/CommandConstants.java | 41 +++++++++++++++++++ .../internal/cli/commands/ProfileMixin.java | 3 +- .../cli/commands/TopLevelCliCommand.java | 3 +- .../cli/commands/TopLevelCliReplCommand.java | 29 ++++++++++++- .../cli/commands/cluster/ClusterUrlMixin.java | 8 +++- .../cluster/init/ClusterInitCommand.java | 6 +-- .../cluster/init/ClusterInitOptions.java | 13 +++--- .../sql/SqlReplTopLevelCliCommand.java | 24 ++++++++++- .../cli/commands/sql/help/SqlHelpCommand.java | 26 +++++++++++- .../cli/commands/sql/SqlCommandTest.java | 2 +- 11 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/CommandConstants.java diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/BaseCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/BaseCommand.java index 8d1ed5a7aed..1280f28ec6a 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/BaseCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/BaseCommand.java @@ -17,6 +17,18 @@ package org.apache.ignite.internal.cli.commands; +import static org.apache.ignite.internal.cli.commands.CommandConstants.ABBREVIATE_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.COMMAND_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.DESCRIPTION_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.HELP_OPTION_ORDER; +import static org.apache.ignite.internal.cli.commands.CommandConstants.OPTION_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.PARAMETER_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.REQUIRED_OPTION_MARKER; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_OPTIONS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SYNOPSIS_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.USAGE_HELP_AUTO_WIDTH; +import static org.apache.ignite.internal.cli.commands.CommandConstants.VERBOSE_OPTION_ORDER; import static org.apache.ignite.internal.cli.commands.Options.Constants.HELP_OPTION; import static org.apache.ignite.internal.cli.commands.Options.Constants.HELP_OPTION_DESC; import static org.apache.ignite.internal.cli.commands.Options.Constants.HELP_OPTION_SHORT; @@ -24,6 +36,7 @@ import static org.apache.ignite.internal.cli.commands.Options.Constants.VERBOSE_OPTION_DESC; import static org.apache.ignite.internal.cli.commands.Options.Constants.VERBOSE_OPTION_SHORT; +import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Spec; @@ -31,13 +44,25 @@ /** * Base class for commands. */ +@Command( + descriptionHeading = DESCRIPTION_HEADING, + optionListHeading = OPTION_LIST_HEADING, + synopsisHeading = SYNOPSIS_HEADING, + requiredOptionMarker = REQUIRED_OPTION_MARKER, + usageHelpAutoWidth = USAGE_HELP_AUTO_WIDTH, + sortOptions = SORT_OPTIONS, + sortSynopsis = SORT_SYNOPSIS, + abbreviateSynopsis = ABBREVIATE_SYNOPSIS, + commandListHeading = COMMAND_LIST_HEADING, + parameterListHeading = PARAMETER_LIST_HEADING +) public abstract class BaseCommand { /** Help option specification. */ - @Option(names = {HELP_OPTION, HELP_OPTION_SHORT}, usageHelp = true, description = HELP_OPTION_DESC) + @Option(names = {HELP_OPTION, HELP_OPTION_SHORT}, description = HELP_OPTION_DESC, usageHelp = true, order = HELP_OPTION_ORDER) protected boolean usageHelpRequested; /** Verbose option specification. */ - @Option(names = {VERBOSE_OPTION, VERBOSE_OPTION_SHORT}, description = VERBOSE_OPTION_DESC) + @Option(names = {VERBOSE_OPTION, VERBOSE_OPTION_SHORT}, description = VERBOSE_OPTION_DESC, order = VERBOSE_OPTION_ORDER) protected boolean verbose; /** Instance of picocli command specification. */ diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/CommandConstants.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/CommandConstants.java new file mode 100644 index 00000000000..b38da58b2ff --- /dev/null +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/CommandConstants.java @@ -0,0 +1,41 @@ +/* + * 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.ignite.internal.cli.commands; + +/** + * Constants for @Command annotation. + */ +public class CommandConstants { + public static final String DESCRIPTION_HEADING = "%nDESCRIPTION%n"; + public static final String OPTION_LIST_HEADING = "%nOPTIONS @|fg(246) * - required option|@ %n"; + public static final String SYNOPSIS_HEADING = "%nUSAGE%n"; + public static final String COMMAND_LIST_HEADING = "%nCOMMANDS%n"; + public static final String PARAMETER_LIST_HEADING = "%nPARAMETERS @|fg(246) * - required parameter|@ %n"; + public static final char REQUIRED_OPTION_MARKER = '*'; + public static final boolean USAGE_HELP_AUTO_WIDTH = true; + public static final boolean SORT_OPTIONS = false; + public static final boolean SORT_SYNOPSIS = false; + public static final boolean ABBREVIATE_SYNOPSIS = true; + + + public static final int CLUSTER_URL_OPTION_ORDER = 10; + public static final int PROFILE_OPTION_ORDER = 11; + public static final int HELP_OPTION_ORDER = 100; + public static final int VERBOSE_OPTION_ORDER = 101; + public static final int VERSION_OPTION_ORDER = 102; +} diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/ProfileMixin.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/ProfileMixin.java index d73e0ffe15e..d3581ece8ec 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/ProfileMixin.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/ProfileMixin.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.cli.commands; +import static org.apache.ignite.internal.cli.commands.CommandConstants.PROFILE_OPTION_ORDER; import static org.apache.ignite.internal.cli.commands.Options.Constants.PROFILE_OPTION; import static org.apache.ignite.internal.cli.commands.Options.Constants.PROFILE_OPTION_DESC; @@ -26,7 +27,7 @@ * Mixin for profile option. */ public class ProfileMixin { - @Option(names = PROFILE_OPTION, description = PROFILE_OPTION_DESC) + @Option(names = PROFILE_OPTION, description = PROFILE_OPTION_DESC, order = PROFILE_OPTION_ORDER) private String profileName; /** diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliCommand.java index 610e13a7247..764eae83cfe 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliCommand.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.cli.commands; +import static org.apache.ignite.internal.cli.commands.CommandConstants.VERSION_OPTION_ORDER; import static org.apache.ignite.internal.cli.commands.Options.Constants.VERSION_OPTION; import static org.apache.ignite.internal.cli.commands.Options.Constants.VERSION_OPTION_DESC; @@ -51,6 +52,6 @@ }) public class TopLevelCliCommand extends BaseCommand { @SuppressWarnings("PMD.UnusedPrivateField") - @Option(names = VERSION_OPTION, versionHelp = true, description = VERSION_OPTION_DESC) + @Option(names = VERSION_OPTION, versionHelp = true, description = VERSION_OPTION_DESC, order = VERSION_OPTION_ORDER) private boolean versionRequested; } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliReplCommand.java index df93c017b3c..0f52531fb08 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliReplCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/TopLevelCliReplCommand.java @@ -17,6 +17,17 @@ package org.apache.ignite.internal.cli.commands; +import static org.apache.ignite.internal.cli.commands.CommandConstants.ABBREVIATE_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.COMMAND_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.DESCRIPTION_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.OPTION_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.PARAMETER_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.REQUIRED_OPTION_MARKER; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_OPTIONS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SYNOPSIS_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.USAGE_HELP_AUTO_WIDTH; + import org.apache.ignite.internal.cli.commands.cliconfig.CliReplCommand; import org.apache.ignite.internal.cli.commands.cluster.ClusterReplCommand; import org.apache.ignite.internal.cli.commands.connect.ConnectReplCommand; @@ -26,12 +37,13 @@ import org.apache.ignite.internal.cli.commands.sql.SqlReplCommand; import org.apache.ignite.internal.cli.commands.version.VersionCommand; import picocli.CommandLine; +import picocli.CommandLine.Command; import picocli.shell.jline3.PicocliCommands; /** * Top-level command that just prints help. */ -@CommandLine.Command(name = "", +@Command(name = "", footer = {"", "Press Ctrl-D to exit."}, subcommands = { SqlReplCommand.class, @@ -45,6 +57,19 @@ NodeReplCommand.class, ClusterReplCommand.class, RecoveryReplCommand.class - }) + }, + + descriptionHeading = DESCRIPTION_HEADING, + optionListHeading = OPTION_LIST_HEADING, + synopsisHeading = SYNOPSIS_HEADING, + requiredOptionMarker = REQUIRED_OPTION_MARKER, + usageHelpAutoWidth = USAGE_HELP_AUTO_WIDTH, + sortOptions = SORT_OPTIONS, + sortSynopsis = SORT_SYNOPSIS, + abbreviateSynopsis = ABBREVIATE_SYNOPSIS, + commandListHeading = COMMAND_LIST_HEADING, + parameterListHeading = PARAMETER_LIST_HEADING + +) public class TopLevelCliReplCommand { } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterUrlMixin.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterUrlMixin.java index 6fa62c6460b..39ea5fdd50d 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterUrlMixin.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/ClusterUrlMixin.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.cli.commands.cluster; +import static org.apache.ignite.internal.cli.commands.CommandConstants.CLUSTER_URL_OPTION_ORDER; import static org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION; import static org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION_DESC; @@ -29,7 +30,12 @@ */ public class ClusterUrlMixin { /** Cluster endpoint URL option. */ - @Option(names = CLUSTER_URL_OPTION, description = CLUSTER_URL_OPTION_DESC, converter = UrlConverter.class) + @Option( + names = CLUSTER_URL_OPTION, + description = CLUSTER_URL_OPTION_DESC, + converter = UrlConverter.class, + order = CLUSTER_URL_OPTION_ORDER + ) private URL clusterUrl; public String getClusterUrl() { diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java index 045a69ec465..b41390fb118 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java @@ -32,12 +32,12 @@ */ @Command(name = "init", description = "Initializes an Ignite cluster") public class ClusterInitCommand extends BaseCommand implements Callable { - /** Cluster endpoint URL option. */ @Mixin - private ClusterUrlProfileMixin clusterUrl; + private ClusterInitOptions clusterInitOptions; + /** Cluster endpoint URL option. */ @Mixin - private ClusterInitOptions clusterInitOptions; + private ClusterUrlProfileMixin clusterUrl; @Inject private ClusterInitCall call; diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java index 21fd207f537..e4f60f220bd 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitOptions.java @@ -53,6 +53,10 @@ * Mixin class for cluster init command options. */ public class ClusterInitOptions { + /** Name of the cluster. */ + @Option(names = CLUSTER_NAME_OPTION, required = true, description = CLUSTER_NAME_OPTION_DESC, order = 0) + private String clusterName; + /** * List of names of the nodes (each represented by a separate command line argument) that will host the Meta Storage. If the * "--cluster-management-group" parameter is omitted, the same nodes will also host the Cluster Management Group. @@ -62,6 +66,7 @@ public class ClusterInitOptions { description = META_STORAGE_NODE_NAME_OPTION_DESC, split = ",", paramLabel = META_STORAGE_NODE_NAME_PARAM_LABEL, + order = 1, preprocessor = SingleOccurrenceMetastorageConsumer.class ) private List metaStorageNodes; @@ -73,24 +78,22 @@ public class ClusterInitOptions { description = CMG_NODE_NAME_OPTION_DESC, split = ",", paramLabel = CMG_NODE_NAME_PARAM_LABEL, + order = 2, preprocessor = SingleOccurrenceClusterManagementConsumer.class ) private List cmgNodes = new ArrayList<>(); - /** Name of the cluster. */ - @Option(names = CLUSTER_NAME_OPTION, required = true, description = CLUSTER_NAME_OPTION_DESC) - private String clusterName; - @ArgGroup private ClusterConfigOptions clusterConfigOptions; private static class ClusterConfigOptions { - @Option(names = CLUSTER_CONFIG_OPTION, description = CLUSTER_CONFIG_OPTION_DESC) + @Option(names = CLUSTER_CONFIG_OPTION, order = 3, description = CLUSTER_CONFIG_OPTION_DESC) private String config; @Option(names = CLUSTER_CONFIG_FILE_OPTION, description = CLUSTER_CONFIG_FILE_OPTION_DESC, split = ",", + order = 4, paramLabel = CLUSTER_CONFIG_FILE_PARAM_LABEL ) private List files; diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplTopLevelCliCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplTopLevelCliCommand.java index 46bb48a4cfd..6357af8c460 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplTopLevelCliCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplTopLevelCliCommand.java @@ -17,6 +17,17 @@ package org.apache.ignite.internal.cli.commands.sql; +import static org.apache.ignite.internal.cli.commands.CommandConstants.ABBREVIATE_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.COMMAND_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.DESCRIPTION_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.OPTION_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.PARAMETER_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.REQUIRED_OPTION_MARKER; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_OPTIONS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SYNOPSIS_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.USAGE_HELP_AUTO_WIDTH; + import org.apache.ignite.internal.cli.commands.ExitCommand; import org.apache.ignite.internal.cli.commands.sql.help.SqlHelpCommand; import picocli.CommandLine.Command; @@ -31,7 +42,18 @@ ClearScreen.class, ExitCommand.class, SqlHelpCommand.class - } + }, + + descriptionHeading = DESCRIPTION_HEADING, + optionListHeading = OPTION_LIST_HEADING, + synopsisHeading = SYNOPSIS_HEADING, + requiredOptionMarker = REQUIRED_OPTION_MARKER, + usageHelpAutoWidth = USAGE_HELP_AUTO_WIDTH, + sortOptions = SORT_OPTIONS, + sortSynopsis = SORT_SYNOPSIS, + abbreviateSynopsis = ABBREVIATE_SYNOPSIS, + commandListHeading = COMMAND_LIST_HEADING, + parameterListHeading = PARAMETER_LIST_HEADING ) public class SqlReplTopLevelCliCommand { } diff --git a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/help/SqlHelpCommand.java b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/help/SqlHelpCommand.java index a62be2b8fc8..157392b8d36 100644 --- a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/help/SqlHelpCommand.java +++ b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/help/SqlHelpCommand.java @@ -17,6 +17,17 @@ package org.apache.ignite.internal.cli.commands.sql.help; +import static org.apache.ignite.internal.cli.commands.CommandConstants.ABBREVIATE_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.COMMAND_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.DESCRIPTION_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.OPTION_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.PARAMETER_LIST_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.REQUIRED_OPTION_MARKER; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_OPTIONS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SORT_SYNOPSIS; +import static org.apache.ignite.internal.cli.commands.CommandConstants.SYNOPSIS_HEADING; +import static org.apache.ignite.internal.cli.commands.CommandConstants.USAGE_HELP_AUTO_WIDTH; + import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; @@ -32,11 +43,22 @@ /** Help command in SQL repl mode. */ @Command(name = "help", header = "Display help information about the specified SQL command.", - synopsisHeading = "%nUsage: ", helpCommand = true, description = { "%nWhen no SQL command is given, the usage help for the main command is displayed.", - "If a SQL command is specified, the help for that command is shown.%n"} + "If a SQL command is specified, the help for that command is shown.%n" + }, + + descriptionHeading = DESCRIPTION_HEADING, + optionListHeading = OPTION_LIST_HEADING, + synopsisHeading = SYNOPSIS_HEADING, + requiredOptionMarker = REQUIRED_OPTION_MARKER, + usageHelpAutoWidth = USAGE_HELP_AUTO_WIDTH, + sortOptions = SORT_OPTIONS, + sortSynopsis = SORT_SYNOPSIS, + abbreviateSynopsis = ABBREVIATE_SYNOPSIS, + commandListHeading = COMMAND_LIST_HEADING, + parameterListHeading = PARAMETER_LIST_HEADING ) public final class SqlHelpCommand implements IHelpCommandInitializable2, Runnable { diff --git a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java index 8cbd62d91b9..30a82bfc6c0 100644 --- a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java +++ b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java @@ -38,7 +38,7 @@ void withoutOptions() { assertAll( () -> assertExitCodeIs(2), this::assertOutputIsEmpty, - () -> assertErrOutputContains("Missing required argument (specify one of these): ( | --file=)") + () -> assertErrOutputContains("Missing required argument (specify one of these): (--file= | )") ); }