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

davsclaus pushed a commit to branch fix/CAMEL-25400-embedded
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 156648647c42fb1e10fc893652f6e2fa1055438c
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 19:19:46 2026 +0200

    CAMEL-25400: camel-jbang - The CLI that runs several commands gives each a 
fresh start
    
    camel ask (cli_exec) and the AI panel of the monitor run Camel CLI commands
    in their own JVM with CliRunner, which reused one command tree: a field of
    a command that is not an option kept the value of the run before. Each run
    now gets new instances of the commands (CamelJBangMain.newCommandLine, with
    the plugins already loaded).
    
    log4j was configured once per JVM, so a later command in such a CLI (or in
    camel shell) that logs otherwise, such as export to its log file, only got
    its level, and logged to the console. The first logging configuration of
    a new command is now applied; a command run on its own behaves as before.
    
    camel doc and the catalog listings load the catalog of their runtime with
    one method.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/CamelJBangMain.java    | 108 ++++++++----
 .../core/commands/CamelShellCommandRegistry.java   |   2 +
 .../camel/dsl/jbang/core/commands/CliRunner.java   |   8 +-
 .../core/commands/catalog/CatalogBaseCommand.java  |   9 +
 .../jbang/core/commands/catalog/CatalogDoc.java    |  21 +--
 .../camel/dsl/jbang/core/common/PluginHelper.java  |  12 +-
 .../camel/dsl/jbang/core/common/RuntimeUtil.java   | 182 +++++++++++++--------
 .../dsl/jbang/core/commands/CliRunnerTest.java     |  35 +++-
 .../jbang/core/common/RuntimeUtilLoggingTest.java  |  58 +++++++
 .../commands/tui/AiCliCommandExecutorTest.java     |  11 +-
 10 files changed, 312 insertions(+), 134 deletions(-)

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 66143a1be5e6..90a71042ec3c 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
@@ -21,6 +21,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
@@ -67,6 +68,7 @@ import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
+import org.apache.camel.dsl.jbang.core.common.Plugin;
 import org.apache.camel.dsl.jbang.core.common.PluginHelper;
 import org.apache.camel.dsl.jbang.core.common.PluginType;
 import org.apache.camel.dsl.jbang.core.common.Printer;
@@ -90,6 +92,9 @@ public class CamelJBangMain implements Callable<Integer> {
 
     private boolean discoverPlugins = true;
 
+    // the plugins whose commands were added, to add them to a new command 
line too
+    private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
+
     public static void run(String... args) {
         run(new CamelJBangMain(), args);
     }
@@ -107,7 +112,51 @@ public class CamelJBangMain implements Callable<Integer> {
             // ignore
         }
 
-        commandLine = new CommandLine(this)
+        commandLine = createCommandLine();
+
+        postAddCommands(commandLine, args);
+
+        if (discoverPlugins && PluginHelper.shouldDiscoverPlugins(commandLine, 
args)) {
+            try {
+                PluginHelper.addPlugins(commandLine, this, args);
+                PluginHelper.installOnFirstUse(commandLine, this, args);
+            } catch (CliException e) {
+                // a plugin that cannot be loaded
+                out.printErr(e.getMessage());
+                quit(e.getExitCode());
+                return;
+            }
+        }
+        withErrorHandling(commandLine);
+
+        commandLine.getCommandSpec().versionProvider(() -> {
+            CamelCatalog catalog = new DefaultCamelCatalog();
+            String v = catalog.getCatalogVersion();
+            return new String[] { v };
+        });
+
+        CommandLineHelper.migrateLegacyUserConfig(out);
+        CommandLineHelper.augmentWithUserConfiguration(commandLine);
+        preExecute(commandLine, args);
+        int exitCode;
+        try {
+            exitCode = commandLine.execute(args);
+        } catch (Error e) {
+            // picocli hands exceptions to the handler, but an error (such as 
a class missing at runtime) escapes it
+            exitCode = new CliExecutionExceptionHandler(this).handle(e, 
commandLine.getParseResult());
+        }
+        if (isHelpRequest(args) && EnvironmentHelper.isInteractiveTerminal()) {
+            printAvailablePlugins();
+        }
+        postExecute(commandLine, args, exitCode);
+        quit(exitCode);
+    }
+
+    /**
+     * The command line with the commands of the CLI.
+     */
+    private CommandLine createCommandLine() {
+        CommandLine commandLine = new CommandLine(this)
                 .addSubcommand("bind", new CommandLine(new Bind(this)))
                 .addSubcommand("catalog", new CommandLine(new 
CatalogCommand(this))
                         .addSubcommand("component", new CommandLine(new 
CatalogComponent(this)))
@@ -256,43 +305,34 @@ public class CamelJBangMain implements Callable<Integer> {
         commandLine.getHelpSectionMap().put(
                 CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST,
                 new GroupedCommandHelpRenderer());
+        return commandLine;
+    }
 
-        postAddCommands(commandLine, args);
-
-        if (discoverPlugins && PluginHelper.shouldDiscoverPlugins(commandLine, 
args)) {
-            try {
-                PluginHelper.addPlugins(commandLine, this, args);
-                PluginHelper.installOnFirstUse(commandLine, this, args);
-            } catch (CliException e) {
-                // a plugin that cannot be loaded
-                out.printErr(e.getMessage());
-                quit(e.getExitCode());
-                return;
-            }
+    /**
+     * A new command line with new instances of the commands of the CLI, and 
of the commands of the plugins this main
+     * has loaded. A command that runs on it starts from its defaults, not 
from what a run before left in its fields:
+     * {@link CliRunner} runs each command on a new command line.
+     */
+    public CommandLine newCommandLine() {
+        CommandLine answer = createCommandLine();
+        postAddCommands(answer, new String[0]);
+        for (Plugin plugin : plugins) {
+            plugin.customize(answer, this);
         }
-        withErrorHandling(commandLine);
-
-        commandLine.getCommandSpec().versionProvider(() -> {
-            CamelCatalog catalog = new DefaultCamelCatalog();
-            String v = catalog.getCatalogVersion();
-            return new String[] { v };
-        });
+        withErrorHandling(answer);
+        answer.getCommandSpec().versionProvider(() -> new String[] { new 
DefaultCamelCatalog().getCatalogVersion() });
+        CommandLineHelper.augmentWithUserConfiguration(answer);
+        return answer;
+    }
 
-        CommandLineHelper.migrateLegacyUserConfig(out);
-        CommandLineHelper.augmentWithUserConfiguration(commandLine);
-        preExecute(commandLine, args);
-        int exitCode;
-        try {
-            exitCode = commandLine.execute(args);
-        } catch (Error e) {
-            // picocli hands exceptions to the handler, but an error (such as 
a class missing at runtime) escapes it
-            exitCode = new CliExecutionExceptionHandler(this).handle(e, 
commandLine.getParseResult());
-        }
-        if (isHelpRequest(args) && EnvironmentHelper.isInteractiveTerminal()) {
-            printAvailablePlugins();
+    /**
+     * Remembers a plugin whose commands were added to the command line, to 
add them to a new command line too (see
+     * {@link #newCommandLine()}).
+     */
+    public void addPlugin(Plugin plugin) {
+        if (!plugins.contains(plugin)) {
+            plugins.add(plugin);
         }
-        postExecute(commandLine, args, exitCode);
-        quit(exitCode);
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
index 25c471865985..7e767214844e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelShellCommandRegistry.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.dsl.jbang.core.common.RuntimeUtil;
 import org.jline.picocli.PicocliCommandRegistry;
 import org.jline.reader.Completer;
 import org.jline.shell.Command;
@@ -121,6 +122,7 @@ public class CamelShellCommandRegistry extends 
PicocliCommandRegistry {
             @Override
             public Object execute(org.jline.shell.CommandSession session, 
String[] args) throws Exception {
                 synchronized (CliRunner.LOCK) {
+                    RuntimeUtil.newCommand();
                     return command.execute(session, args);
                 }
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java
index 7adcaf05e52a..8603e8cc1e9d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliRunner.java
@@ -27,6 +27,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.Printer;
+import org.apache.camel.dsl.jbang.core.common.RuntimeUtil;
 import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.apache.camel.util.json.JsonObject;
 import picocli.CommandLine;
@@ -103,10 +104,12 @@ public final class CliRunner {
      * @param timeoutMillis how long the command may run, or 0 to wait until 
it has finished
      */
     public static CliResult run(List<String> args, long timeoutMillis) {
-        CommandLine commandLine = CamelJBangMain.getCommandLine();
-        if (commandLine == null) {
+        CommandLine main = CamelJBangMain.getCommandLine();
+        if (main == null) {
             throw new IllegalStateException("The Camel CLI is not 
initialized");
         }
+        // new instances of the commands, so that no field keeps the value a 
run before left in it
+        CommandLine commandLine = ((CamelJBangMain) 
main.getCommand()).newCommandLine();
         Output output = new Output();
         long start = System.nanoTime();
         if (timeoutMillis <= 0) {
@@ -175,6 +178,7 @@ public final class CliRunner {
             commandLine.setOut(output.helpOut);
             commandLine.setErr(output.helpErr);
             try {
+                RuntimeUtil.newCommand();
                 return commandLine.execute(args.toArray(String[]::new));
             } finally {
                 output.helpOut.flush();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
index 20df3f945ba4..73b96f15471b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java
@@ -115,6 +115,15 @@ public abstract class CatalogBaseCommand extends 
CamelCommand {
     }
 
     CamelCatalog loadCatalog() throws Exception {
+        return loadCatalog(runtime, camelVersion, quarkusPlatform, 
mavenResolver);
+    }
+
+    /**
+     * The catalog of the runtime and Camel version: of Spring Boot, of the 
Quarkus platform, or of Camel.
+     */
+    static CamelCatalog loadCatalog(
+            RuntimeType runtime, String camelVersion, QuarkusPlatformMixin 
quarkusPlatform, MavenResolverMixin mavenResolver)
+            throws Exception {
         if (RuntimeType.springBoot == runtime) {
             return CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), 
camelVersion, mavenResolver.download());
         } else if (RuntimeType.quarkus == runtime) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
index 4149fcd2aec8..5de4bbdd0b93 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
@@ -30,14 +30,12 @@ import com.github.freva.asciitable.Column;
 import com.github.freva.asciitable.HorizontalAlign;
 import com.github.freva.asciitable.OverflowBehaviour;
 import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
 import org.apache.camel.dsl.jbang.core.commands.QuarkusPlatformMixin;
 import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs;
 import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
-import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
 import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
@@ -129,24 +127,7 @@ public class CatalogDoc extends CamelCommand {
     }
 
     CamelCatalog loadCatalog() throws Exception {
-        if (RuntimeType.springBoot == runtime) {
-            return CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), 
camelVersion, mavenResolver.download());
-        } else if (RuntimeType.quarkus == runtime) {
-            final MavenGav quarkusCamelBom
-                    = quarkusPlatform
-                            .resolve(
-                                    camelVersion,
-                                    
mavenResolver.downloader()::resolveArtifact,
-                                    mavenResolver.download(),
-                                    mavenResolver.fresh())
-                            .quarkusCamelBom();
-            return CatalogLoader.loadQuarkusCatalog(quarkusCamelBom, 
mavenResolver.downloader()::resolveArtifact);
-        }
-        if (camelVersion == null) {
-            return new DefaultCamelCatalog(true);
-        } else {
-            return CatalogLoader.loadCatalog(mavenResolver.repos(), 
camelVersion, mavenResolver.download());
-        }
+        return CatalogBaseCommand.loadCatalog(runtime, camelVersion, 
quarkusPlatform, mavenResolver);
     }
 
     @Override
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
index 6cc8c474c0a1..4bd5a3349c94 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
@@ -154,7 +154,7 @@ public final class PluginHelper {
                 continue;
             }
 
-            plugin.getValue().customize(commandLine, main);
+            apply(plugin.getValue(), commandLine, main);
         }
     }
 
@@ -180,12 +180,18 @@ public final class PluginHelper {
         main.getOut().printDiagnostic("Installed plugin: " + 
pluginType.get().getName());
         for (Map.Entry<String, Plugin> plugin : getActivePlugins(main, 
resolveRepos(args), target).entrySet()) {
             if (!commandLine.getSubcommands().containsKey(plugin.getKey())) {
-                plugin.getValue().customize(commandLine, main);
+                apply(plugin.getValue(), commandLine, main);
             }
         }
         return true;
     }
 
+    // adds the commands of the plugin, and the main remembers the plugin for 
the command lines it makes later
+    private static void apply(Plugin plugin, CommandLine commandLine, 
CamelJBangMain main) {
+        plugin.customize(commandLine, main);
+        main.addPlugin(plugin);
+    }
+
     // allow to download plugins from 3rd party maven repositories by --repos 
argument, or the user configuration
     private static String resolveRepos(String... args) {
         String repos = null;
@@ -800,7 +806,7 @@ public final class PluginHelper {
                     return true;
                 }
 
-                plugin.customize(commandLine, main);
+                apply(plugin, commandLine, main);
                 return true;
             }
         } catch (Exception e) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java
index b2c8fbe3b25e..1737f18ef6e1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtil.java
@@ -20,11 +20,13 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.StringReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.*;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.util.IOHelper;
 import org.apache.camel.util.OrderedProperties;
@@ -34,7 +36,10 @@ import org.apache.logging.log4j.core.config.Configurator;
 
 public final class RuntimeUtil {
 
-    private static final AtomicBoolean INIT_DONE = new AtomicBoolean();
+    // the logging configuration log4j was configured with, or null when not 
yet
+    private static String configured;
+    // whether the command that runs next may configure the logging again (see 
newCommand)
+    private static boolean newCommand;
 
     private RuntimeUtil() {
     }
@@ -50,84 +55,119 @@ public final class RuntimeUtil {
             String level, boolean color, boolean json, boolean script, boolean 
export, String loggingConfigPath,
             List<String> loggingCategories, boolean mcpStdio)
             throws Exception {
-        if (INIT_DONE.compareAndSet(false, true)) {
-            long pid = ProcessHandle.current().pid();
-            System.setProperty("pid", Long.toString(pid));
-            // make it possible to manage log4j via JMX and camel-cli
-            System.setProperty("log4j2.disable.jmx", "false");
-
-            if (loggingConfigPath != null) {
-                // ust custom logging configuration as-is
-                Configurator.initialize("CamelJBang", "file://" + 
Path.of(loggingConfigPath).toAbsolutePath());
-            } else if (mcpStdio) {
-                Configurator.initialize("CamelJBang", 
"log4j2-mcp-stdio.properties");
-            } else if (loggingCategories != null && 
!loggingCategories.isEmpty()) {
-                // enrich logging file with custom logging categories
-                String name = "log4j2-no-color.properties";
-                if (export) {
-                    name = "log4j2-export.properties";
-                } else if (script) {
-                    name = "log4j2-script.properties";
-                } else if (json) {
-                    name = "log4j2-json.properties";
-                } else if (color) {
-                    name = "log4j2.properties";
-                }
-                InputStream is = 
RuntimeUtil.class.getClassLoader().getResourceAsStream(name);
-                String content = IOHelper.loadText(is);
-                IOHelper.close(is);
-
-                StringJoiner sj = new StringJoiner(System.lineSeparator());
-                int i = 0;
-                for (String lc : loggingCategories) {
-                    String prefix = "custom" + i++;
-                    String catName = StringHelper.before(lc, "=", "").trim();
-                    String catLevel = StringHelper.after(lc, "=", "").trim();
-                    if ("root".equalsIgnoreCase(catName)) {
-                        // log4j allows one root logger only, which the 
template already defines: a root category is
-                        // the root logging level
-                        if (!catLevel.isEmpty()) {
-                            level = catLevel;
-                        }
-                        continue;
-                    }
-                    if (!catName.isEmpty() && !catLevel.isEmpty()) {
-                        sj.add("logger." + prefix + ".name=" + catName);
-                        sj.add("logger." + prefix + ".level=" + catLevel);
-                        // the category gets the same appenders as the root 
logger, so it must not also pass its
-                        // events up to the root, or every line is printed 
twice (CAMEL-24701)
-                        sj.add("logger." + prefix + ".additivity=false");
-                        if (!export && !script) {
-                            sj.add("logger." + prefix + 
".appenderRef.$1.ref=out");
-                        }
-                        sj.add("logger." + prefix + 
".appenderRef.$2.ref=file");
+        String location;
+        if (loggingConfigPath != null) {
+            // ust custom logging configuration as-is
+            location = 
Path.of(loggingConfigPath).toAbsolutePath().toUri().toString();
+        } else if (mcpStdio) {
+            location = "log4j2-mcp-stdio.properties";
+        } else if (loggingCategories != null && !loggingCategories.isEmpty()) {
+            // enrich logging file with custom logging categories
+            String name = "log4j2-no-color.properties";
+            if (export) {
+                name = "log4j2-export.properties";
+            } else if (script) {
+                name = "log4j2-script.properties";
+            } else if (json) {
+                name = "log4j2-json.properties";
+            } else if (color) {
+                name = "log4j2.properties";
+            }
+            InputStream is = 
RuntimeUtil.class.getClassLoader().getResourceAsStream(name);
+            String content = IOHelper.loadText(is);
+            IOHelper.close(is);
+
+            StringJoiner sj = new StringJoiner(System.lineSeparator());
+            int i = 0;
+            for (String lc : loggingCategories) {
+                String prefix = "custom" + i++;
+                String catName = StringHelper.before(lc, "=", "").trim();
+                String catLevel = StringHelper.after(lc, "=", "").trim();
+                if ("root".equalsIgnoreCase(catName)) {
+                    // log4j allows one root logger only, which the template 
already defines: a root category is
+                    // the root logging level
+                    if (!catLevel.isEmpty()) {
+                        level = catLevel;
                     }
+                    continue;
                 }
-                content = content + System.lineSeparator() + sj;
-
-                name = CommandLineHelper.CAMEL_JBANG_WORK_DIR + 
"/log4j2.properties";
-                Files.writeString(Paths.get(name), content);
-
-                Configurator.initialize("CamelJBang", 
Path.of(name).toAbsolutePath().toUri().toString());
-            } else {
-                // use out of the box logging configuration
-                if (export) {
-                    Configurator.initialize("CamelJBang", 
"log4j2-export.properties");
-                } else if (script) {
-                    Configurator.initialize("CamelJBang", 
"log4j2-script.properties");
-                } else if (json) {
-                    Configurator.initialize("CamelJBang", 
"log4j2-json.properties");
-                } else if (color) {
-                    Configurator.initialize("CamelJBang", "log4j2.properties");
-                } else {
-                    Configurator.initialize("CamelJBang", 
"log4j2-no-color.properties");
+                if (!catName.isEmpty() && !catLevel.isEmpty()) {
+                    sj.add("logger." + prefix + ".name=" + catName);
+                    sj.add("logger." + prefix + ".level=" + catLevel);
+                    // the category gets the same appenders as the root 
logger, so it must not also pass its
+                    // events up to the root, or every line is printed twice 
(CAMEL-24701)
+                    sj.add("logger." + prefix + ".additivity=false");
+                    if (!export && !script) {
+                        sj.add("logger." + prefix + ".appenderRef.$1.ref=out");
+                    }
+                    sj.add("logger." + prefix + ".appenderRef.$2.ref=file");
                 }
             }
+            content = content + System.lineSeparator() + sj;
+
+            name = CommandLineHelper.CAMEL_JBANG_WORK_DIR + 
"/log4j2.properties";
+            Files.writeString(Paths.get(name), content);
+            // the categories are part of what the command asks for
+            location = Path.of(name).toAbsolutePath().toUri() + "#" + 
content.hashCode();
+        } else if (export) {
+            // use out of the box logging configuration
+            location = "log4j2-export.properties";
+        } else if (script) {
+            location = "log4j2-script.properties";
+        } else if (json) {
+            location = "log4j2-json.properties";
+        } else if (color) {
+            location = "log4j2.properties";
+        } else {
+            location = "log4j2-no-color.properties";
+        }
+
+        synchronized (RuntimeUtil.class) {
+            if (configured == null) {
+                long pid = ProcessHandle.current().pid();
+                System.setProperty("pid", Long.toString(pid));
+                // make it possible to manage log4j via JMX and camel-cli
+                System.setProperty("log4j2.disable.jmx", "false");
+                Configurator.initialize("CamelJBang", withoutHash(location));
+                configured = location;
+            } else if (newCommand && !configured.equals(location)) {
+                // a CLI that runs several commands in this JVM (camel ask, 
camel shell, the monitor) runs one that
+                // logs otherwise, such as export to its log file: log4j is 
configured once, so configure it again
+                Configurator.reconfigure(toUri(withoutHash(location)));
+                configured = location;
+            }
+            // as for a command run on its own, the first configuration of the 
command is the one it logs with
+            newCommand = false;
         }
 
         setRootLoggingLevel(level);
     }
 
+    /**
+     * A CLI that runs several commands in this JVM (camel ask, camel shell, 
the monitor) runs another one: its first
+     * logging configuration is applied, as for a command run on its own, also 
when log4j was configured otherwise by a
+     * command before.
+     */
+    public static void newCommand() {
+        synchronized (RuntimeUtil.class) {
+            newCommand = true;
+        }
+    }
+
+    private static String withoutHash(String location) {
+        int pos = location.lastIndexOf('#');
+        return pos > 0 ? location.substring(0, pos) : location;
+    }
+
+    // a configuration on the classpath, or a file
+    private static URI toUri(String location) throws URISyntaxException {
+        if (location.startsWith("file:")) {
+            return new URI(location);
+        }
+        URL url = RuntimeUtil.class.getClassLoader().getResource(location);
+        return url != null ? url.toURI() : new URI(location);
+    }
+
     public static void setRootLoggingLevel(String level) {
         if (level != null) {
             level = level.toLowerCase();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java
index 0cbd6ebc43fd..583fdee0e177 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliRunnerTest.java
@@ -139,6 +139,13 @@ class CliRunnerTest {
         assertThat(config).doesNotExist();
     }
 
+    @Test
+    void everyRunHasNewCommands() {
+        // a field that is not an option keeps its value only when the same 
instance runs again
+        
assertThat(CliRunner.run(List.of("count")).stdout()).isEqualTo("runs=1");
+        
assertThat(CliRunner.run(List.of("count")).stdout()).isEqualTo("runs=1");
+    }
+
     @Test
     void resultAsJson() {
         JsonObject json = new CliRunner.CliResult(3, "out", "x".repeat(10), 
12, false).toJson(5);
@@ -153,8 +160,9 @@ class CliRunnerTest {
     private static class TestMain extends CamelJBangMain {
         @Override
         public void postAddCommands(CommandLine commandLine, String[] args) {
-            // the commands of the CLI, and one that ignores the interrupt
+            // the commands of the CLI, one that ignores the interrupt, and 
one that counts its runs
             commandLine.addSubcommand("busy", new CommandLine(new 
BusyCommand(this)));
+            commandLine.addSubcommand("count", new CommandLine(new 
CountCommand(this)));
         }
 
         @Override
@@ -163,6 +171,31 @@ class CliRunnerTest {
         }
     }
 
+    /**
+     * A command that counts its runs in a field that is not an option.
+     */
+    @CommandLine.Command(name = "count")
+    static class CountCommand extends CamelCommand {
+
+        private int runs;
+
+        CountCommand(CamelJBangMain main) {
+            super(main);
+        }
+
+        @Override
+        protected boolean disarrangeLogging() {
+            return false;
+        }
+
+        @Override
+        public Integer doCall() {
+            runs++;
+            printer().println("runs=" + runs);
+            return 0;
+        }
+    }
+
     /**
      * A command that runs for 3 seconds whatever happens, as one blocked in 
I/O does.
      */
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java
new file mode 100644
index 000000000000..f41f7d2cbf93
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeUtilLoggingTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.common;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The logging of a command: configured once, as a command runs on its own, 
but a CLI that runs several commands in this
+ * JVM (camel ask, camel shell, the monitor) gives each of them its own.
+ */
+class RuntimeUtilLoggingTest {
+
+    @Test
+    void newCommandOfACliThatRunsSeveralCommandsLogsAsOnItsOwn() throws 
Exception {
+        RuntimeUtil.configureLog("off", false, false, false, false, null, 
null);
+        Set<String> first = appenders();
+
+        // a command run on its own keeps the configuration it started with
+        RuntimeUtil.configureLog("info", false, false, false, true, null, 
null);
+        assertThat(appenders()).isEqualTo(first);
+
+        // the next command of camel ask or the monitor logs as it would on 
its own: export to its log file only
+        RuntimeUtil.newCommand();
+        RuntimeUtil.configureLog("info", false, false, false, true, null, 
null);
+        assertThat(appenders()).containsExactly("file");
+
+        // and the one after it logs to the console again
+        RuntimeUtil.newCommand();
+        RuntimeUtil.configureLog("off", false, false, false, false, null, 
null);
+        assertThat(appenders()).contains("out");
+    }
+
+    private static Set<String> appenders() {
+        LoggerContext context = (LoggerContext) LogManager.getContext(false);
+        return new 
HashSet<>(context.getConfiguration().getAppenders().keySet());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
index c59d87960362..cf6d3ad05e1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiCliCommandExecutorTest.java
@@ -136,10 +136,15 @@ class AiCliCommandExecutorTest {
 
     @Test
     void productionInvokerRestoresPrinterAndDoesNotQuitMain() throws Exception 
{
-        CamelJBangMain main = new CamelJBangMain();
+        // a command runs on new instances of the commands, so the test 
command is added the way plugins add theirs
+        CamelJBangMain main = new CamelJBangMain() {
+            @Override
+            public void postAddCommands(CommandLine commandLine, String[] 
args) {
+                commandLine.addSubcommand("tui-test", new CommandLine(new 
TuiTestCommand(this)));
+            }
+        };
         Printer originalPrinter = main.getOut();
-        CommandLine commandLine = new CommandLine(main)
-                .addSubcommand("tui-test", new CommandLine(new 
TuiTestCommand(main)));
+        CommandLine commandLine = new CommandLine(main);
         Field field = CamelJBangMain.class.getDeclaredField("commandLine");
         field.setAccessible(true);
         CommandLine previous = (CommandLine) field.get(null);

Reply via email to