From 65813730bd0ad39b492b0b58cf48f1c7e2ed3f0e Mon Sep 17 00:00:00 2001 From: sk-reddy17 Date: Sat, 27 Jun 2026 21:02:56 +0530 Subject: [PATCH 1/2] CAMEL-23654: camel-jbang - group commands in --help output by category for better navigation --- .../jbang/core/commands/CamelJBangMain.java | 4 + .../commands/GroupedCommandHelpRenderer.java | 95 +++++++++++++++++++ .../GroupedCommandHelpRendererTest.java | 68 +++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 02e883ddda72e..1007f4798ad04 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -228,6 +228,10 @@ public void execute(String... args) { .addSubcommand("wrapper", new CommandLine(new WrapperCommand(this))) .setParameterExceptionHandler(new MissingPluginParameterExceptionHandler()); + commandLine.getHelpSectionMap().put( + CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST, + new GroupedCommandHelpRenderer()); + postAddCommands(commandLine, args); if (discoverPlugins && PluginHelper.shouldDiscoverPlugins(commandLine, args)) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java new file mode 100644 index 0000000000000..14c64d725406f --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java @@ -0,0 +1,95 @@ +/* + * 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.camel.dsl.jbang.core.commands; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import picocli.CommandLine; + +/** + * Renders the camel CLI subcommand list grouped by functional category, replacing the default flat alphabetical list. + */ +public class GroupedCommandHelpRenderer implements CommandLine.IHelpSectionRenderer { + + private static final Map> GROUPS = new LinkedHashMap<>(); + + static { + GROUPS.put("Running", List.of("run", "dev", "stop", "restart", "ps", "log", "shell", "script")); + GROUPS.put("Monitoring", List.of("get", "top", "trace", "hawtio", "jolokia")); + GROUPS.put("Actions", List.of("cmd", "bind")); + GROUPS.put("Development", + List.of("init", "export", "debug", "eval", "explain", "transform", "dirty", "doctor", "sbom", "nano")); + GROUPS.put("Configuration", List.of("config", "dependency", "version", "update", "wrapper", "completion", "plugin")); + GROUPS.put("Catalog", List.of("catalog", "doc", "infra")); + GROUPS.put("AI", List.of("ask", "harden")); + } + + @Override + public String render(CommandLine.Help help) { + Map subcommands = help.commandSpec().subcommands(); + if (subcommands.isEmpty()) { + return ""; + } + + int nameWidth = subcommands.keySet().stream().mapToInt(String::length).max().orElse(10); + + List assigned = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + + for (Map.Entry> entry : GROUPS.entrySet()) { + List present = new ArrayList<>(); + for (String name : entry.getValue()) { + CommandLine sub = subcommands.get(name); + if (sub != null) { + present.add(sub); + assigned.add(name); + } + } + if (!present.isEmpty()) { + sb.append(String.format("%n %s:%n", entry.getKey())); + for (CommandLine sub : present) { + appendCommand(sb, sub, nameWidth); + } + } + } + + List ungrouped = new ArrayList<>(); + for (Map.Entry entry : subcommands.entrySet()) { + if (!assigned.contains(entry.getKey())) { + ungrouped.add(entry.getValue()); + } + } + if (!ungrouped.isEmpty()) { + sb.append(String.format("%n Other:%n")); + for (CommandLine sub : ungrouped) { + appendCommand(sb, sub, nameWidth); + } + } + + return sb.toString(); + } + + private static void appendCommand(StringBuilder sb, CommandLine sub, int nameWidth) { + String name = sub.getCommandSpec().name(); + String[] desc = sub.getCommandSpec().usageMessage().description(); + String description = desc != null && desc.length > 0 ? desc[0] : ""; + sb.append(String.format(" %-" + nameWidth + "s %s%n", name, description)); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java new file mode 100644 index 0000000000000..98bc038c4f414 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java @@ -0,0 +1,68 @@ +/* + * 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.camel.dsl.jbang.core.commands; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GroupedCommandHelpRendererTest extends CamelCommandBaseTestSupport { + + private String captureHelp() { + StringWriter sw = new StringWriter(); + CamelJBangMain main = new CamelJBangMain() { + @Override + public void quit(int exitCode) { + } + + @Override + public void postAddCommands(CommandLine commandLine, String[] args) { + commandLine.setOut(new PrintWriter(sw)); + } + }.withPrinter(printer); + main.setDiscoverPlugins(false); + main.execute("--help"); + return sw.toString(); + } + + @Test + public void helpOutputContainsGroups() throws Exception { + String output = captureHelp(); + + assertTrue(output.contains("Running:"), "Missing 'Running' group"); + assertTrue(output.contains("Monitoring:"), "Missing 'Monitoring' group"); + assertTrue(output.contains("Actions:"), "Missing 'Actions' group"); + assertTrue(output.contains("Development:"), "Missing 'Development' group"); + assertTrue(output.contains("Configuration:"), "Missing 'Configuration' group"); + assertTrue(output.contains("Catalog:"), "Missing 'Catalog' group"); + assertTrue(output.contains("AI:"), "Missing 'AI' group"); + } + + @Test + public void helpOutputContainsKeyCommands() throws Exception { + String output = captureHelp(); + + assertTrue(output.contains("run"), "Missing 'run' command"); + assertTrue(output.contains("get"), "Missing 'get' command"); + assertTrue(output.contains("config"), "Missing 'config' command"); + assertTrue(output.contains("catalog"), "Missing 'catalog' command"); + } +} From e0790acc96b5694a4758fe7603f41a2e84644f95 Mon Sep 17 00:00:00 2001 From: sk-reddy17 Date: Sun, 28 Jun 2026 09:15:36 +0530 Subject: [PATCH 2/2] CAMEL-23654: camel-jbang - skip hidden commands in grouped help and add tests --- .../commands/GroupedCommandHelpRenderer.java | 9 ++++-- .../GroupedCommandHelpRendererTest.java | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java index 14c64d725406f..72c7a2e6081aa 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRenderer.java @@ -58,7 +58,11 @@ public String render(CommandLine.Help help) { for (String name : entry.getValue()) { CommandLine sub = subcommands.get(name); if (sub != null) { - present.add(sub); + // don't print hidden commands (picocli hides them too), but still + // count them as assigned so they don't pop up under "Other" + if (!sub.getCommandSpec().usageMessage().hidden()) { + present.add(sub); + } assigned.add(name); } } @@ -72,7 +76,8 @@ public String render(CommandLine.Help help) { List ungrouped = new ArrayList<>(); for (Map.Entry entry : subcommands.entrySet()) { - if (!assigned.contains(entry.getKey())) { + if (!assigned.contains(entry.getKey()) + && !entry.getValue().getCommandSpec().usageMessage().hidden()) { ungrouped.add(entry.getValue()); } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java index 98bc038c4f414..03368439c4797 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/GroupedCommandHelpRendererTest.java @@ -18,14 +18,18 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Map; import org.junit.jupiter.api.Test; import picocli.CommandLine; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class GroupedCommandHelpRendererTest extends CamelCommandBaseTestSupport { + private CommandLine capturedCommandLine; + private String captureHelp() { StringWriter sw = new StringWriter(); CamelJBangMain main = new CamelJBangMain() { @@ -36,6 +40,7 @@ public void quit(int exitCode) { @Override public void postAddCommands(CommandLine commandLine, String[] args) { commandLine.setOut(new PrintWriter(sw)); + capturedCommandLine = commandLine; } }.withPrinter(printer); main.setDiscoverPlugins(false); @@ -65,4 +70,29 @@ public void helpOutputContainsKeyCommands() throws Exception { assertTrue(output.contains("config"), "Missing 'config' command"); assertTrue(output.contains("catalog"), "Missing 'catalog' command"); } + + @Test + public void noBuiltInCommandFallsIntoOther() throws Exception { + String output = captureHelp(); + + // Every command should belong to a category, so we never expect to see "Other". + // If this fails, someone added a command but forgot to put it in a group. + assertFalse(output.contains("Other:"), + "An unmapped command fell into the 'Other' group — add it to a category in GroupedCommandHelpRenderer"); + } + + @Test + public void allRegisteredCommandsAppearInHelp() throws Exception { + String output = captureHelp(); + + // Make sure no command quietly goes missing: every registered, non-hidden + // command should appear somewhere in the grouped output. + for (Map.Entry entry : capturedCommandLine.getSubcommands().entrySet()) { + if (entry.getValue().getCommandSpec().usageMessage().hidden()) { + continue; + } + String name = entry.getValue().getCommandSpec().name(); + assertTrue(output.contains(name), "Command '" + name + "' is missing from the grouped help output"); + } + } }