You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by vk...@apache.org on 2020/12/22 01:50:09 UTC

[ignite-3] branch main updated: CLI tool: help improvements

This is an automated email from the ASF dual-hosted git repository.

vkulichenko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new cedb629  CLI tool: help improvements
cedb629 is described below

commit cedb629ecf1d680bfe07148cf955caf5969c964d
Author: Valentin Kulichenko <va...@gmail.com>
AuthorDate: Mon Dec 21 17:49:48 2020 -0800

    CLI tool: help improvements
---
 .../java/org/apache/ignite/cli/ErrorHandler.java   |  12 +-
 .../org/apache/ignite/cli/HelpFactoryImpl.java     | 228 ++++++++++-----------
 .../src/main/java/org/apache/ignite/cli/Table.java |  75 +++++--
 ...nitIgniteCommandSpec.java => CategorySpec.java} |  21 +-
 .../{AbstractCommandSpec.java => CommandSpec.java} |   9 +-
 .../apache/ignite/cli/spec/ConfigCommandSpec.java  |  12 +-
 .../org/apache/ignite/cli/spec/IgniteCliSpec.java  |  13 +-
 .../ignite/cli/spec/InitIgniteCommandSpec.java     |   3 +-
 .../apache/ignite/cli/spec/ModuleCommandSpec.java  |  14 +-
 .../apache/ignite/cli/spec/NodeCommandSpec.java    |  15 +-
 .../org/apache/ignite/cli/spec/SpecAdapter.java    |  55 +++++
 .../java/org/apache/ignite/app/IgniteRunner.java   |   4 +-
 12 files changed, 262 insertions(+), 199 deletions(-)

diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
index eba44d7..ae68a02 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/ErrorHandler.java
@@ -19,7 +19,7 @@ package org.apache.ignite.cli;
 
 import javax.inject.Inject;
 import io.micronaut.context.ApplicationContext;
-import org.apache.ignite.cli.spec.IgniteCliSpec;
+import org.apache.ignite.cli.spec.CategorySpec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine;
@@ -45,9 +45,15 @@ public class ErrorHandler implements CommandLine.IExecutionExceptionHandler, Com
     @Override public int handleParseException(CommandLine.ParameterException ex, String[] args) {
         CommandLine cli = ex.getCommandLine();
 
-        cli.getErr().println(cli.getColorScheme().errorText("ERROR: ") + ex.getMessage() + '\n');
+        if (cli.getCommand() instanceof CategorySpec) {
+            ((Runnable)cli.getCommand()).run();
+        }
+        else {
+            cli.getErr().println(cli.getColorScheme().errorText("[ERROR] ") + ex.getMessage() +
+                ". Please see usage information below.\n");
 
-        cli.usage(cli.getOut());
+            cli.usage(cli.getOut());
+        }
 
         return cli.getCommandSpec().exitCodeOnInvalidInput();
     }
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
index a5c40b5..e2f1583 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/HelpFactoryImpl.java
@@ -1,167 +1,147 @@
 package org.apache.ignite.cli;
 
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
-import java.util.Optional;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-import java.util.function.ToIntFunction;
-import java.util.stream.Collectors;
+import java.util.List;
+import java.util.Map;
+import org.apache.ignite.cli.spec.SpecAdapter;
 import picocli.CommandLine;
+import picocli.CommandLine.Help.Ansi;
+import picocli.CommandLine.Help.ColorScheme;
+import picocli.CommandLine.Model.OptionSpec;
+import picocli.CommandLine.Model.PositionalParamSpec;
 
 public class HelpFactoryImpl implements CommandLine.IHelpFactory {
-
     public static final String SECTION_KEY_BANNER = "banner";
-    public static final String SECTION_KEY_SYNOPSIS_EXTENSION = "synopsisExt";
-
-    private static final String[] BANNER = new String[] {
-        "                       ___                         __",
-        "                      /   |   ____   ____ _ _____ / /_   ___",
-        "  @|red,bold       ⣠⣶⣿|@          / /| |  / __ \\ / __ `// ___// __ \\ / _ \\",
-        "  @|red,bold      ⣿⣿⣿⣿|@         / ___ | / /_/ // /_/ // /__ / / / //  __/",
-        "  @|red,bold  ⢠⣿⡏⠈⣿⣿⣿⣿⣷|@       /_/  |_|/ .___/ \\__,_/ \\___//_/ /_/ \\___/",
-        "  @|red,bold ⢰⣿⣿⣿⣧⠈⢿⣿⣿⣿⣿⣦|@            /_/",
-        "  @|red,bold ⠘⣿⣿⣿⣿⣿⣦⠈⠛⢿⣿⣿⣿⡄|@       ____               _  __           _____",
-        "  @|red,bold  ⠈⠛⣿⣿⣿⣿⣿⣿⣦⠉⢿⣿⡟|@      /  _/____ _ ____   (_)/ /_ ___     |__  /",
-        "  @|red,bold ⢰⣿⣶⣀⠈⠙⠿⣿⣿⣿⣿ ⠟⠁|@      / / / __ `// __ \\ / // __// _ \\     /_ <",
-        "  @|red,bold ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟|@       _/ / / /_/ // / / // // /_ /  __/   ___/ /",
-        "  @|red,bold       ⠉⠉⠛⠏⠉|@      /___/ \\__, //_/ /_//_/ \\__/ \\___/   /____/",
-        "                        /____/\n"};
-
-    @Override public CommandLine.Help create(CommandLine.Model.CommandSpec commandSpec,
-        CommandLine.Help.ColorScheme colorScheme) {
+    public static final String SECTION_KEY_PARAMETER_OPTION_TABLE = "paramsOptsTable";
+
+    @Override public CommandLine.Help create(CommandLine.Model.CommandSpec commandSpec, ColorScheme cs) {
+        boolean hasCommands = !commandSpec.subcommands().isEmpty();
+        boolean hasOptions = commandSpec.options().stream().anyMatch(o -> !o.hidden());
+        boolean hasParameters = commandSpec.positionalParameters().stream().anyMatch(o -> !o.hidden());
+
+        // Any command can have either subcommands or options/parameters, but not both.
+        assert !(hasCommands && (hasOptions || hasParameters));
+
         commandSpec.usageMessage().sectionKeys(Arrays.asList(
             SECTION_KEY_BANNER,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
             CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
-            SECTION_KEY_SYNOPSIS_EXTENSION,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
-            CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST));
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+            CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST,
+            SECTION_KEY_PARAMETER_OPTION_TABLE
+        ));
 
         var sectionMap = new HashMap<String, CommandLine.IHelpSectionRenderer>();
 
-        boolean hasCommands = !commandSpec.subcommands().isEmpty();
-        boolean hasOptions = commandSpec.options().stream().anyMatch(o -> !o.hidden());
-        boolean hasParameters = !commandSpec.positionalParameters().isEmpty();
-
-        sectionMap.put(SECTION_KEY_BANNER,
-            help -> Arrays
-                .stream(BANNER)
-                .map(CommandLine.Help.Ansi.AUTO::string)
-                .collect(Collectors.joining("\n")) +
-                "\n"
-        );
-
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_HEADER,
-            help -> CommandLine.Help.Ansi.AUTO.string(
-                Arrays.stream(help.commandSpec().version())
-                    .collect(Collectors.joining("\n")) + "\n\n")
-        );
+        if (commandSpec.commandLine().isUsageHelpRequested()) {
+            sectionMap.put(SECTION_KEY_BANNER,
+                help -> {
+                    assert help.commandSpec().commandLine().getCommand() instanceof SpecAdapter;
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
-            help -> CommandLine.Help.Ansi.AUTO.string("@|bold,green " + help.commandSpec().qualifiedName() +
-                "|@\n  " + help.description() + "\n"));
+                    return ((SpecAdapter)help.commandSpec().commandLine().getCommand()).banner();
+                }
+            );
+        }
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS_HEADING,
-            help -> CommandLine.Help.Ansi.AUTO.string("@|bold USAGE|@\n"));
+        if (!hasCommands) {
+            sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
+                help -> {
+                    StringBuilder sb = new StringBuilder();
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS,
-            help -> {
-                StringBuilder sb = new StringBuilder();
+                    List<Ansi.IStyle> boldCmdStyle = new ArrayList<>(cs.commandStyles());
 
-                sb.append("  ");
-                sb.append(help.colorScheme().commandText(help.commandSpec().qualifiedName()));
+                    boldCmdStyle.add(Ansi.Style.bold);
+
+                    sb.append(cs.apply(help.commandSpec().qualifiedName(), boldCmdStyle));
 
-                if (hasCommands) {
-                    sb.append(help.colorScheme().commandText(" <COMMAND>"));
-                }
-                else {
                     if (hasOptions)
-                        sb.append(help.colorScheme().optionText(" [OPTIONS]"));
+                        sb.append(cs.optionText(" [OPTIONS]"));
 
                     if (hasParameters) {
-                        for (CommandLine.Model.PositionalParamSpec parameter : commandSpec.positionalParameters())
-                            sb.append(' ').append(help.colorScheme().parameterText(parameter.paramLabel()));
+                        for (PositionalParamSpec parameter : commandSpec.positionalParameters())
+                            sb.append(' ').append(cs.parameterText(parameter.paramLabel()));
                     }
-                }
 
-                sb.append("\n\n");
-
-                return sb.toString();
-            });
+                    sb.append("\n\n");
 
+                    return sb.toString();
+                }
+            );
+        }
 
-        Optional.ofNullable(commandSpec.usageMessage().sectionMap().get(SECTION_KEY_SYNOPSIS_EXTENSION))
-            .ifPresent(v -> sectionMap.put(SECTION_KEY_SYNOPSIS_EXTENSION, v));
+        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION,
+            help -> Ansi.AUTO.string(help.description() + '\n'));
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST_HEADING,
-            help -> hasParameters ? CommandLine.Help.Ansi.AUTO.string("@|bold REQUIRED PARAMETERS|@\n") : "");
+        if (hasCommands) {
+            sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, help -> {
+                Table table = new Table(0, cs);
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_PARAMETER_LIST, new TableRenderer<>(
-            h -> h.commandSpec().positionalParameters(),
-            p -> p.paramLabel().length(),
-            (h, p) -> h.colorScheme().parameterText(p.paramLabel()),
-            (h, p) -> h.colorScheme().text(p.description()[0])));
+                table.addSection("@|bold COMMANDS|@");
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST_HEADING,
-            help -> hasOptions ? CommandLine.Help.Ansi.AUTO.string("@|bold OPTIONS|@\n") : "");
+                for (Map.Entry<String, CommandLine.Help> entry : help.subcommands().entrySet()) {
+                    String name = entry.getKey();
+                    CommandLine.Help cmd = entry.getValue();
 
-        if (hasOptions)
-            sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST, new TableRenderer<>(
-                h -> h.commandSpec().options(),
-                o -> o.shortestName().length() + o.paramLabel().length() + 1,
-                (h, o) -> h.colorScheme().optionText(o.shortestName()).concat("=").concat(o.paramLabel()),
-                (h, o) -> h.colorScheme().text(o.description()[0])));
+                    if (cmd.subcommands().isEmpty()) {
+                        table.addRow(cs.commandText(name), cmd.description().trim());
+                    }
+                    else {
+                        for (Map.Entry<String, CommandLine.Help> subEntry : cmd.subcommands().entrySet()) {
+                            String subName = subEntry.getKey();
+                            CommandLine.Help subCmd = subEntry.getValue();
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING,
-            help -> hasCommands ? CommandLine.Help.Ansi.AUTO.string("@|bold COMMANDS|@\n") : "");
+                            // Further hierarchy is prohibited.
+                            assert subCmd.subcommands().isEmpty();
 
-        sectionMap.put(CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, new TableRenderer<>(
-            h -> h.subcommands().values(),
-            c -> c.commandSpec().name().length(),
-            (h, c) -> h.colorScheme().commandText(c.commandSpec().name()),
-            (h, c) -> h.colorScheme().text(c.description().stripTrailing())));
-        commandSpec.usageMessage().sectionMap(sectionMap);
-        return new CommandLine.Help(commandSpec, colorScheme);
-    }
+                            table.addRow(cs.commandText(name + " " + subName), subCmd.description().trim());
+                        }
+                    }
+                }
 
-    private static class TableRenderer<T> implements CommandLine.IHelpSectionRenderer {
-        private final Function<CommandLine.Help, Collection<T>> itemsFunc;
-        private final ToIntFunction<T> nameLenFunc;
-        private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc;
-        private final BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc;
-
-        TableRenderer(Function<CommandLine.Help, Collection<T>> itemsFunc, ToIntFunction<T> nameLenFunc,
-            BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> nameFunc, BiFunction<CommandLine.Help, T, CommandLine.Help.Ansi.Text> descriptionFunc) {
-            this.itemsFunc = itemsFunc;
-            this.nameLenFunc = nameLenFunc;
-            this.nameFunc = nameFunc;
-            this.descriptionFunc = descriptionFunc;
+                return table.toString();
+            });
         }
+        else if (hasParameters || hasOptions) {
+            sectionMap.put(SECTION_KEY_PARAMETER_OPTION_TABLE, help -> {
+                Table table = new Table(0, cs);
 
-        @Override public String render(CommandLine.Help help) {
-            Collection<T> items = itemsFunc.apply(help);
+                if (hasParameters) {
+                    table.addSection("@|bold REQUIRED PARAMETERS|@");
 
-            if (items.isEmpty())
-                return "";
+                    for (PositionalParamSpec param : help.commandSpec().positionalParameters()) {
+                        if (!param.hidden()) {
+                            // TODO: Support multiple-line descriptions.
+                            assert param.description().length == 1;
 
-            int len = 2 + items.stream().mapToInt(nameLenFunc).max().getAsInt();
+                            table.addRow(cs.parameterText(param.paramLabel()), param.description()[0]);
+                        }
+                    }
+                }
+
+                if (hasOptions) {
+                    table.addSection("@|bold OPTIONS|@");
 
-            CommandLine.Help.TextTable table = CommandLine.Help.TextTable.forColumns(help.colorScheme(),
-                new CommandLine.Help.Column(len, 2, CommandLine.Help.Column.Overflow.SPAN),
-                new CommandLine.Help.Column(160 - len, 4, CommandLine.Help.Column.Overflow.WRAP));
+                    for (OptionSpec option : help.commandSpec().options()) {
+                        if (!option.hidden()) {
+                            // TODO: Support multiple names.
+                            assert option.names().length == 1;
+                            // TODO: Support multiple-line descriptions.
+                            assert option.description().length == 1;
 
-            for (T item : items)
-                table.addRowValues(nameFunc.apply(help, item), descriptionFunc.apply(help, item));
+                            table.addRow(
+                                cs.optionText(option.names()[0] + '=' + option.paramLabel()),
+                                option.description()[0]);
+                        }
+                    }
+                }
 
-            return table.toString() + '\n';
+                return table.toString();
+            });
         }
+
+        commandSpec.usageMessage().sectionMap(sectionMap);
+
+        return new CommandLine.Help(commandSpec, cs);
     }
 }
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
index af43004..cc4cffb 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/Table.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.cli;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import picocli.CommandLine.Help.Ansi.Text;
 import picocli.CommandLine.Help.ColorScheme;
@@ -30,7 +31,7 @@ public class Table {
 
     private final ColorScheme colorScheme;
 
-    private final Collection<Text[]> data = new ArrayList<>();
+    private final Collection<Row> data = new ArrayList<>();
 
     private int[] lengths;
 
@@ -76,7 +77,18 @@ public class Table {
             lengths[i] = Math.max(lengths[i], text.getCJKAdjustedLength());
         }
 
-        data.add(row);
+        data.add(new DataRow(row));
+    }
+
+    /**
+     * Adds a section. Title spans all columns in the table.
+     *
+     * @param title Section title.
+     */
+    public void addSection(Object title) {
+        Text text = title instanceof Text ? (Text)title : colorScheme.text(title.toString());
+
+        data.add(new SectionTitle(text));
     }
 
     /**
@@ -85,13 +97,12 @@ public class Table {
      * @return String representation of this table.
      */
     @Override public String toString() {
-        String indentStr = " ".repeat(indent);
+        if (data.isEmpty())
+            return "";
 
         StringBuilder sb = new StringBuilder();
 
-        for (Text[] row : data) {
-            sb.append(indentStr);
-
+        for (Row row : data) {
             appendLine(sb);
             appendRow(sb, row);
         }
@@ -102,6 +113,8 @@ public class Table {
     }
 
     private void appendLine(StringBuilder sb) {
+        sb.append(" ".repeat(indent));
+
         for (int length : lengths) {
             sb.append('+').append("-".repeat(length + 2));
         }
@@ -109,15 +122,53 @@ public class Table {
         sb.append("+\n");
     }
 
-    private void appendRow(StringBuilder sb, Text[] row) {
-        assert row.length == lengths.length;
+    private void appendRow(StringBuilder sb, Row row) {
+        sb.append(" ".repeat(indent))
+          .append(row.render())
+          .append('\n');
+    }
+
+    private interface Row {
+        String render();
+    }
+
+    private class DataRow implements Row {
+        private final Text[] row;
 
-        for (int i = 0; i < row.length; i++) {
-            Text item = row[i];
+        DataRow(Text[] row) {
+            this.row = row;
+        }
+
+        @Override public String render() {
+            assert row.length == lengths.length;
+
+            StringBuilder sb = new StringBuilder();
+
+            for (int i = 0; i < row.length; i++) {
+                Text item = row[i];
+
+                sb.append("| ")
+                  .append(item.toString())
+                  .append(" ".repeat(lengths[i] + 1 - item.getCJKAdjustedLength()));
+            }
+
+            sb.append("|");
 
-            sb.append("| ").append(item.toString()).append(" ".repeat(lengths[i] + 1 - item.getCJKAdjustedLength()));
+            return sb.toString();
         }
+    }
+
+    private class SectionTitle implements Row {
+        private final Text title;
+
+        SectionTitle(Text title) {
+            this.title = title;
+        }
+
+        @Override public String render() {
+            int totalLen = Arrays.stream(lengths).sum() + 3 * (lengths.length - 1);
 
-        sb.append("|\n");
+            return "| " + title.toString() + " ".repeat(totalLen - title.getCJKAdjustedLength()) + " |";
+        }
     }
 }
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CategorySpec.java
similarity index 64%
copy from modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
copy to modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CategorySpec.java
index af1aefd..0b42d7a 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CategorySpec.java
@@ -17,20 +17,17 @@
 
 package org.apache.ignite.cli.spec;
 
-import javax.inject.Inject;
-import io.micronaut.context.ApplicationContext;
-import org.apache.ignite.cli.common.IgniteCommand;
-import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
-import picocli.CommandLine;
+import java.io.PrintWriter;
+import picocli.CommandLine.Help.ColorScheme;
 
-@CommandLine.Command(name = "init", description = "Install Ignite core modules locally.")
-public class InitIgniteCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+public abstract class CategorySpec extends SpecAdapter {
+    @Override public void run() {
+        PrintWriter out = spec.commandLine().getOut();
+        ColorScheme cs = spec.commandLine().getColorScheme();
 
-    @Inject
-    InitIgniteCommand command;
+        out.println(cs.errorText("[ERROR] ") + "Unknown command: " +
+            cs.commandText(spec.qualifiedName()) + ". See the list of available commands below.\n");
 
-    @Override public void run() {
-        command.init(spec.commandLine().getOut());
+        spec.parent().commandLine().usage(out);
     }
-
 }
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CommandSpec.java
similarity index 76%
rename from modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
rename to modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CommandSpec.java
index 4cc905d..50541d8 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/AbstractCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/CommandSpec.java
@@ -17,12 +17,9 @@
 
 package org.apache.ignite.cli.spec;
 
-import org.apache.ignite.cli.VersionProvider;
 import picocli.CommandLine;
-import picocli.CommandLine.Model.CommandSpec;
 
-@CommandLine.Command(versionProvider = VersionProvider.class)
-public abstract class AbstractCommandSpec implements Runnable {
-    @CommandLine.Spec
-    protected CommandSpec spec;
+public abstract class CommandSpec extends SpecAdapter {
+    @CommandLine.Option(names = "--help", usageHelp = true, hidden = true)
+    protected boolean usageHelpRequested;
 }
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
index aa08afc..abd5bb1 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ConfigCommandSpec.java
@@ -18,7 +18,6 @@
 package org.apache.ignite.cli.spec;
 
 import javax.inject.Inject;
-import io.micronaut.context.ApplicationContext;
 import org.apache.ignite.cli.IgniteCLIException;
 import org.apache.ignite.cli.builtins.config.ConfigurationClient;
 import picocli.CommandLine;
@@ -31,14 +30,9 @@ import picocli.CommandLine;
         ConfigCommandSpec.SetConfigCommandSpec.class
     }
 )
-public class ConfigCommandSpec extends AbstractCommandSpec {
-
-    @Override public void run() {
-        spec.commandLine().usage(spec.commandLine().getOut());
-    }
-
+public class ConfigCommandSpec extends CategorySpec {
     @CommandLine.Command(name = "get", description = "Get current Ignite cluster configuration values.")
-    public static class GetConfigCommandSpec extends AbstractCommandSpec {
+    public static class GetConfigCommandSpec extends CommandSpec {
 
         @Inject private ConfigurationClient configurationClient;
 
@@ -59,7 +53,7 @@ public class ConfigCommandSpec extends AbstractCommandSpec {
         name = "set",
         description = "Update Ignite cluster configuration values."
     )
-    public static class SetConfigCommandSpec extends AbstractCommandSpec {
+    public static class SetConfigCommandSpec extends CommandSpec {
 
         @Inject private ConfigurationClient configurationClient;
 
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
index ba25e51..657348c 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/IgniteCliSpec.java
@@ -36,14 +36,12 @@ import org.apache.ignite.cli.builtins.module.ModuleStorage;
 import org.apache.ignite.cli.common.IgniteCommand;
 import picocli.CommandLine;
 
-import static org.apache.ignite.cli.HelpFactoryImpl.SECTION_KEY_SYNOPSIS_EXTENSION;
-
 /**
  *
  */
 @CommandLine.Command(
     name = "ignite",
-    description = "Entry point.",
+    description = "Type @|green ignite <COMMAND>|@ @|yellow --help|@ to get help for any command.",
     subcommands = {
         InitIgniteCommandSpec.class,
         ModuleCommandSpec.class,
@@ -51,18 +49,15 @@ import static org.apache.ignite.cli.HelpFactoryImpl.SECTION_KEY_SYNOPSIS_EXTENSI
         ConfigCommandSpec.class,
     }
 )
-public class IgniteCliSpec extends AbstractCommandSpec {
-
+public class IgniteCliSpec extends CommandSpec {
     @CommandLine.Option(names = "-i", hidden = true, required = false)
     boolean interactive;
 
     @Override public void run() {
-        spec.usageMessage().sectionMap().put(SECTION_KEY_SYNOPSIS_EXTENSION,
-            help -> " Or type " + help.colorScheme().commandText(spec.qualifiedName()) +
-                    ' ' + help.colorScheme().parameterText("-i") + " to enter interactive mode.\n\n");
-
         CommandLine cli = spec.commandLine();
 
+        cli.getOut().print(banner());
+
         if (interactive)
             new InteractiveWrapper().run(cli);
         else
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
index af1aefd..488bd88 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/InitIgniteCommandSpec.java
@@ -18,13 +18,12 @@
 package org.apache.ignite.cli.spec;
 
 import javax.inject.Inject;
-import io.micronaut.context.ApplicationContext;
 import org.apache.ignite.cli.common.IgniteCommand;
 import org.apache.ignite.cli.builtins.init.InitIgniteCommand;
 import picocli.CommandLine;
 
 @CommandLine.Command(name = "init", description = "Install Ignite core modules locally.")
-public class InitIgniteCommandSpec extends AbstractCommandSpec implements IgniteCommand {
+public class InitIgniteCommandSpec extends CommandSpec implements IgniteCommand {
 
     @Inject
     InitIgniteCommand command;
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
index ab114ff..07f608c 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/ModuleCommandSpec.java
@@ -25,7 +25,6 @@ import javax.inject.Inject;
 import com.github.freva.asciitable.AsciiTable;
 import com.github.freva.asciitable.Column;
 import com.github.freva.asciitable.HorizontalAlign;
-import io.micronaut.context.ApplicationContext;
 import org.apache.ignite.cli.CliPathsConfigLoader;
 import org.apache.ignite.cli.builtins.module.ModuleManager;
 import org.apache.ignite.cli.common.IgniteCommand;
@@ -40,14 +39,9 @@ import picocli.CommandLine;
         ModuleCommandSpec.RemoveModuleCommandSpec.class
     }
 )
-public class ModuleCommandSpec extends AbstractCommandSpec implements IgniteCommand {
-
-    @Override public void run() {
-        spec.commandLine().usage(spec.commandLine().getOut());
-    }
-
+public class ModuleCommandSpec extends CategorySpec implements IgniteCommand {
     @CommandLine.Command(name = "add", description = "Add an optional Ignite module or an external artifact.")
-    public static class AddModuleCommandSpec extends AbstractCommandSpec {
+    public static class AddModuleCommandSpec extends CommandSpec {
 
         @Inject private ModuleManager moduleManager;
 
@@ -72,7 +66,7 @@ public class ModuleCommandSpec extends AbstractCommandSpec implements IgniteComm
     }
 
     @CommandLine.Command(name = "remove", description = "Add an optional Ignite module or an external artifact.")
-    public static class RemoveModuleCommandSpec extends AbstractCommandSpec {
+    public static class RemoveModuleCommandSpec extends CommandSpec {
 
         @Inject private ModuleManager moduleManager;
 
@@ -89,7 +83,7 @@ public class ModuleCommandSpec extends AbstractCommandSpec implements IgniteComm
     }
 
     @CommandLine.Command(name = "list", description = "Show the list of available optional Ignite modules.")
-    public static class ListModuleCommandSpec extends AbstractCommandSpec {
+    public static class ListModuleCommandSpec extends CommandSpec {
 
         @Inject private ModuleManager moduleManager;
 
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
index 5b761f3..00f4b21 100644
--- a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/NodeCommandSpec.java
@@ -38,14 +38,9 @@ import picocli.CommandLine;
         NodeCommandSpec.ListNodesCommandSpec.class
     }
 )
-public class NodeCommandSpec extends AbstractCommandSpec {
-
-    @Override public void run() {
-        spec.commandLine().usage(spec.commandLine().getOut());
-    }
-
+public class NodeCommandSpec extends CategorySpec {
     @CommandLine.Command(name = "start", description = "Start an Ignite node locally.")
-    public static class StartNodeCommandSpec extends AbstractCommandSpec {
+    public static class StartNodeCommandSpec extends CommandSpec {
 
         @Inject private CliPathsConfigLoader cliPathsConfigLoader;
 
@@ -71,7 +66,7 @@ public class NodeCommandSpec extends AbstractCommandSpec {
     }
 
     @CommandLine.Command(name = "stop", description = "Stop a locally running Ignite node.")
-    public static class StopNodeCommandSpec extends AbstractCommandSpec {
+    public static class StopNodeCommandSpec extends CommandSpec {
 
         @Inject private NodeManager nodeManager;
         @Inject private CliPathsConfigLoader cliPathsConfigLoader;
@@ -93,7 +88,7 @@ public class NodeCommandSpec extends AbstractCommandSpec {
     }
 
     @CommandLine.Command(name = "list", description = "Show the list of currently running local Ignite nodes.")
-    public static class ListNodesCommandSpec extends AbstractCommandSpec {
+    public static class ListNodesCommandSpec extends CommandSpec {
 
         @Inject private NodeManager nodeManager;
         @Inject private CliPathsConfigLoader cliPathsConfigLoader;
@@ -121,7 +116,7 @@ public class NodeCommandSpec extends AbstractCommandSpec {
     }
 
     @CommandLine.Command(name = "classpath", description = "Show the current classpath used by the Ignite nodes.")
-    public static class NodesClasspathCommandSpec extends AbstractCommandSpec {
+    public static class NodesClasspathCommandSpec extends CommandSpec {
 
         @Inject private NodeManager nodeManager;
 
diff --git a/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/SpecAdapter.java b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/SpecAdapter.java
new file mode 100644
index 0000000..f0c8157
--- /dev/null
+++ b/modules/cli-demo/cli/src/main/java/org/apache/ignite/cli/spec/SpecAdapter.java
@@ -0,0 +1,55 @@
+/*
+ * 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.cli.spec;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.apache.ignite.cli.VersionProvider;
+import picocli.CommandLine;
+import picocli.CommandLine.Help.Ansi;
+import picocli.CommandLine.Model.CommandSpec;
+
+@CommandLine.Command(versionProvider = VersionProvider.class)
+public abstract class SpecAdapter implements Runnable {
+    private static final String[] BANNER = new String[] {
+        "                       ___                         __",
+        "                      /   |   ____   ____ _ _____ / /_   ___",
+        "  @|red,bold       ⣠⣶⣿|@          / /| |  / __ \\ / __ `// ___// __ \\ / _ \\",
+        "  @|red,bold      ⣿⣿⣿⣿|@         / ___ | / /_/ // /_/ // /__ / / / // ___/",
+        "  @|red,bold  ⢠⣿⡏⠈⣿⣿⣿⣿⣷|@       /_/  |_|/ .___/ \\__,_/ \\___//_/ /_/ \\___/",
+        "  @|red,bold ⢰⣿⣿⣿⣧⠈⢿⣿⣿⣿⣿⣦|@            /_/",
+        "  @|red,bold ⠘⣿⣿⣿⣿⣿⣦⠈⠛⢿⣿⣿⣿⡄|@       ____               _  __           @|red,bold _____|@",
+        "  @|red,bold  ⠈⠛⣿⣿⣿⣿⣿⣿⣦⠉⢿⣿⡟|@      /  _/____ _ ____   (_)/ /_ ___     @|red,bold |__  /|@",
+        "  @|red,bold ⢰⣿⣶⣀⠈⠙⠿⣿⣿⣿⣿ ⠟⠁|@      / / / __ `// __ \\ / // __// _ \\     @|red,bold /_ <|@",
+        "  @|red,bold ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟|@       _/ / / /_/ // / / // // /_ / ___/   @|red,bold ___/ /|@",
+        "  @|red,bold       ⠉⠉⠛⠏⠉|@      /___/ \\__, //_/ /_//_/ \\__/ \\___/   @|red,bold /____/|@",
+        "                        /____/\n"
+    };
+
+    @CommandLine.Spec
+    protected CommandSpec spec;
+
+    public String banner() {
+        String banner = Arrays
+            .stream(BANNER)
+            .map(Ansi.AUTO::string)
+            .collect(Collectors.joining("\n"));
+
+        return banner + '\n' + " ".repeat(19) + spec.version()[0] + "\n\n";
+    }
+}
diff --git a/modules/ignite-runner/src/main/java/org/apache/ignite/app/IgniteRunner.java b/modules/ignite-runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
index 02d8877..061e69c 100644
--- a/modules/ignite-runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
+++ b/modules/ignite-runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
@@ -36,13 +36,13 @@ public class IgniteRunner {
         "                       ___                         __\n" +
         "                      /   |   ____   ____ _ _____ / /_   ___\n" +
         "        ⣠⣶⣿          / /| |  / __ \\ / __ `// ___// __ \\ / _ \\\n" +
-        "       ⣿⣿⣿⣿         / ___ | / /_/ // /_/ // /__ / / / //  __/\n" +
+        "       ⣿⣿⣿⣿         / ___ | / /_/ // /_/ // /__ / / / // ___/\n" +
         "   ⢠⣿⡏⠈⣿⣿⣿⣿⣷       /_/  |_|/ .___/ \\__,_/ \\___//_/ /_/ \\___/\n" +
         "  ⢰⣿⣿⣿⣧⠈⢿⣿⣿⣿⣿⣦            /_/\n" +
         "  ⠘⣿⣿⣿⣿⣿⣦⠈⠛⢿⣿⣿⣿⡄       ____               _  __           _____\n" +
         "   ⠈⠛⣿⣿⣿⣿⣿⣿⣦⠉⢿⣿⡟      /  _/____ _ ____   (_)/ /_ ___     |__  /\n" +
         "  ⢰⣿⣶⣀⠈⠙⠿⣿⣿⣿⣿ ⠟⠁      / / / __ `// __ \\ / // __// _ \\     /_ <\n" +
-        "  ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟       _/ / / /_/ // / / // // /_ /  __/   ___/ /\n" +
+        "  ⠈⠻⣿⣿⣿⣿⣷⣤⠙⢿⡟       _/ / / /_/ // / / // // /_ / ___/   ___/ /\n" +
         "        ⠉⠉⠛⠏⠉      /___/ \\__, //_/ /_//_/ \\__/ \\___/   /____/\n" +
         "                        /____/\n\n";