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

davsclaus pushed a commit to branch se
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 20fb3158d9efb24bdd8b136af38a2df60897276e
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Feb 5 13:46:42 2026 +0100

    CAMEL-22935: camel-jbang - Add eval expression command
---
 .../apache/camel/catalog/dev-consoles.properties   |   1 +
 .../camel/catalog/dev-consoles/eval-language.json  |  15 ++
 .../impl/engine/DefaultSimpleFunctionRegistry.java |   3 +-
 .../apache/camel/dev-console/eval-language.json    |  15 ++
 .../org/apache/camel/dev-console/eval-language     |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../camel/impl/console/EvalLanguageDevConsole.java | 133 ++++++++++++
 .../impl/console/SimpleLanguageDevConsole.java     |  12 +-
 .../java/org/apache/camel/util/SimpleUtils.java    |   4 +-
 .../pages/jbang-commands/camel-jbang-commands.adoc |   1 +
 .../camel-jbang-eval-expression.adoc               |  32 +++
 .../pages/jbang-commands/camel-jbang-eval.adoc     |  34 ++++
 .../camel-jbang-transform-message.adoc             |   2 +-
 .../camel/cli/connector/LocalCliConnector.java     |  27 +++
 .../META-INF/camel-jbang-commands-metadata.json    |   3 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../camel/dsl/jbang/core/commands/EvalCommand.java |  36 ++++
 .../commands/action/EvalExpressionCommand.java     | 224 +++++++++++++++++++++
 .../commands/action/TransformMessageAction.java    |   2 +-
 .../dsl/jbang/core/commands/EvalSimpleTest.java    |  47 +++++
 .../ParameterExceptionHandlerTest.java             |   2 +-
 21 files changed, 587 insertions(+), 12 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index eccfcfb03b00..6954ed2b5d4e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -10,6 +10,7 @@ consumer
 context
 debug
 endpoint
+eval-language
 event
 fault-tolerance
 gc
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
new file mode 100644
index 000000000000..6082795c18bb
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "eval-language",
+    "title": "Evaluate Language",
+    "description": "Evaluate Language and display result",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.EvalLanguageDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.18.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
index 143f8a110d6d..706d14c14521 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
@@ -19,6 +19,7 @@ package org.apache.camel.impl.engine;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.camel.CamelContext;
@@ -127,7 +128,7 @@ public class DefaultSimpleFunctionRegistry extends 
ServiceSupport implements Sim
 
     @Override
     public Set<String> getCustomFunctionNames() {
-        return functions.keySet();
+        return new TreeSet<>(functions.keySet());
     }
 
     @Override
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
new file mode 100644
index 000000000000..6082795c18bb
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "eval-language",
+    "title": "Evaluate Language",
+    "description": "Evaluate Language and display result",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.EvalLanguageDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.18.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
new file mode 100644
index 000000000000..19ba1cc8d860
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.EvalLanguageDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index d1f4d2b43665..c5c4e618f264 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context debug 
endpoint event gc health inflight internal-tasks java-security jvm log memory 
message-history processor producer properties receive reload rest route 
route-controller route-dump route-group route-structure send service 
simple-language source startup-recorder system-properties thread top trace 
transformers type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context debug 
endpoint eval-language event gc health inflight internal-tasks java-security 
jvm log memory message-history processor producer properties receive reload 
rest route route-controller route-dump route-group route-structure send service 
simple-language source startup-recorder system-properties thread top trace 
transformers type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.18.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
new file mode 100644
index 000000000000..0040eace7604
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
@@ -0,0 +1,133 @@
+/*
+ * 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.impl.console;
+
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "eval-language", displayName = "Evaluate Language", 
description = "Evaluate Language and display result")
+public class EvalLanguageDevConsole extends AbstractDevConsole {
+
+    /**
+     * The language to use
+     */
+    public static final String LANGUAGE = "language";
+
+    /**
+     * Template to use for executing simple language function
+     */
+    public static final String TEMPLATE = "template";
+
+    /**
+     * Whether to execute as predicate (use expression by default)
+     */
+    public static final String PREDICATE = "predicate";
+
+    /**
+     * Optional message body
+     */
+    public static final String BODY = "body";
+
+    /**
+     * Optional message headers
+     */
+    public static final String HEADERS = "headers";
+
+    public EvalLanguageDevConsole() {
+        super("camel", "eval-language", "Evaluate Language", "Evaluate 
Language and display result");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        StringBuilder sb = new StringBuilder();
+
+        String language = (String) options.getOrDefault(LANGUAGE, "simple");
+        String template = (String) options.get(TEMPLATE);
+        if (template != null) {
+            Exchange dummy = new DefaultExchange(getCamelContext());
+            dummy.getMessage().setBody(options.get(BODY));
+            var headers = options.get(HEADERS);
+            if (headers instanceof Map map) {
+                dummy.getMessage().setHeaders(map);
+            }
+
+            String out;
+            boolean predicate = options.getOrDefault(PREDICATE, 
"false").equals("true");
+            if (predicate) {
+                Predicate pre = 
getCamelContext().resolveLanguage(language).createPredicate(template);
+                out = pre.matches(dummy) ? "true" : "false";
+            } else {
+                Expression exp = 
getCamelContext().resolveLanguage(language).createExpression(template);
+                out = exp.evaluate(dummy, String.class);
+            }
+            sb.append(String.format("%nEvaluating (%s): %s", language, 
template));
+            sb.append("\n");
+            sb.append(out);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String language = (String) options.getOrDefault(LANGUAGE, "simple");
+        String template = (String) options.get(TEMPLATE);
+        if (template != null) {
+            Exchange dummy = new DefaultExchange(getCamelContext());
+            dummy.getMessage().setBody(options.get(BODY));
+            var headers = options.get(HEADERS);
+            if (headers instanceof Map map) {
+                dummy.getMessage().setHeaders(map);
+            }
+
+            Exception cause = null;
+            String out = null;
+            try {
+                boolean predicate = options.getOrDefault(PREDICATE, 
"false").equals("true");
+                if (predicate) {
+                    Predicate pre = 
getCamelContext().resolveLanguage(language).createPredicate(template);
+                    out = pre.matches(dummy) ? "true" : "false";
+                } else {
+                    Expression exp = 
getCamelContext().resolveLanguage(language).createExpression(template);
+                    out = exp.evaluate(dummy, String.class);
+                }
+            } catch (Exception e) {
+                cause = e;
+            }
+
+            if (cause != null) {
+                root.put("status", "failed");
+                root.put("exception",
+                        
MessageHelper.dumpExceptionAsJSonObject(cause).getMap("exception"));
+            } else {
+                root.put("status", "success");
+                root.put("result", out);
+            }
+        }
+
+        return root;
+    }
+}
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
index 517c1eaea2fb..4577a5e54d98 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
@@ -39,20 +39,23 @@ public class SimpleLanguageDevConsole extends 
AbstractDevConsole {
         SimpleFunctionRegistry reg = 
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
         sb.append(String.format("%n    Core Functions: %d", reg.coreSize()));
         for (String name : reg.getCoreFunctionNames()) {
-            sb.append(String.format("%n    %s", name));
+            sb.append(String.format("%n        %s", name));
         }
+        sb.append("\n");
         sb.append(String.format("%n    Custom Functions: %d", 
reg.customSize()));
         for (String name : reg.getCustomFunctionNames()) {
-            sb.append(String.format("%n    %s", name));
+            sb.append(String.format("%n        %s", name));
         }
+        sb.append("\n");
+
         return sb.toString();
     }
 
     @Override
     protected JsonObject doCallJson(Map<String, Object> options) {
-        SimpleFunctionRegistry reg = 
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
-
         JsonObject root = new JsonObject();
+
+        SimpleFunctionRegistry reg = 
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
         root.put("coreSize", reg.coreSize());
         root.put("customSize", reg.customSize());
         JsonArray arr = new JsonArray();
@@ -65,6 +68,7 @@ public class SimpleLanguageDevConsole extends 
AbstractDevConsole {
         if (!arr.isEmpty()) {
             root.put("customFunctions", arr);
         }
+
         return root;
     }
 }
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java 
b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
index 7319c067c4ed..b55cba73b9d4 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
@@ -18,12 +18,12 @@ package org.apache.camel.util;
 
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 public class SimpleUtils {
 
-    private static final Set<String> SIMPLE_FUNCTIONS = 
Collections.unmodifiableSet(new HashSet<>(
+    private static final Set<String> SIMPLE_FUNCTIONS = 
Collections.unmodifiableSet(new LinkedHashSet<>(
             Arrays.asList(
                     // Generated by camel build tools - do NOT edit this list!
                     // SIMPLE-FUNCTIONS: START
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
index d519eb557fe6..5f0c3ac8c98e 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
@@ -20,6 +20,7 @@ TIP: You can also use `camel --help` or `camel <command> 
--help` to see availabl
 | xref:jbang-commands/camel-jbang-dependency.adoc[camel dependency] | Displays 
all Camel dependencies required to run
 | xref:jbang-commands/camel-jbang-dirty.adoc[camel dirty] | Check if there are 
dirty files from previous Camel runs that did not terminate gracefully
 | xref:jbang-commands/camel-jbang-doc.adoc[camel doc] | Shows documentation 
for kamelet, component, and other Camel resources
+| xref:jbang-commands/camel-jbang-eval.adoc[camel eval] | Evaluate Camel 
expressions and scripts
 | xref:jbang-commands/camel-jbang-explain.adoc[camel explain] | Explain what a 
Camel route does using AI/LLM
 | xref:jbang-commands/camel-jbang-export.adoc[camel export] | Export to other 
runtimes (Camel Main, Spring Boot, or Quarkus)
 | xref:jbang-commands/camel-jbang-get.adoc[camel get] | Get status of Camel 
integrations
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
new file mode 100644
index 000000000000..ea2b9e869141
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
@@ -0,0 +1,32 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel eval expression
+
+Evaluates Camel expression
+
+
+== Usage
+
+[source,bash]
+----
+camel eval expression [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--body` | Message body (prefix with file: to refer to loading message body 
from file) |  | String
+| `--camel-version` | To run using a different Camel version than the default 
version. |  | String
+| `--header` | Message header (key=value) |  | List
+| `--repo,--repos` | Additional maven repositories (Use commas to separate 
multiple repositories) |  | String
+| `--template` | The template to use for evaluating (prefix with file: to 
refer to loading template from file) |  | String
+| `--timeout` | Timeout in millis waiting for evaluation to be done | 10000 | 
long
+| `--watch` | Execute periodically and showing output fullscreen |  | boolean
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc
new file mode 100644
index 000000000000..20ca09b59751
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc
@@ -0,0 +1,34 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel eval
+
+Evaluate Camel expressions and scripts
+
+
+== Usage
+
+[source,bash]
+----
+camel eval [options]
+----
+
+
+== Subcommands
+
+[cols="2,5",options="header"]
+|===
+| Subcommand | Description
+| xref:jbang-commands/camel-jbang-eval-expression.adoc[expression] | Evaluates 
Camel expression
+|===
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
index 2e50c308475e..68d27963cb00 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
@@ -35,7 +35,7 @@ camel transform message [options]
 | `--show-exchange-properties` | Show exchange properties from the output 
message | false | boolean
 | `--show-headers` | Show message headers from the output message | true | 
boolean
 | `--source` | Instead of using external template file then refer to an 
existing Camel route source with inlined Camel language expression in a route. 
(use :line-number or :id to refer to the exact location of the EIP to use) |  | 
String
-| `--template` | The template to use for message transformation (prefix with 
file: to refer to loading message body from file) |  | String
+| `--template` | The template to use for message transformation (prefix with 
file: to refer to loading template from file) |  | String
 | `--timeout` | Timeout in millis waiting for message to be transformed | 
20000 | long
 | `--watch` | Execute periodically and showing output fullscreen |  | boolean
 | `-h,--help` | Display the help and sub-commands |  | boolean
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index b12e69a6f7c7..eaf6e37df925 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -292,6 +292,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionLoggerTask(root);
             } else if ("gc".equals(action)) {
                 System.gc();
+            } else if ("eval".equals(action)) {
+                doActionEvalTask(root);
             } else if ("load".equals(action)) {
                 doActionLoadTask(root);
             } else if ("reload".equals(action)) {
@@ -856,6 +858,31 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionEvalTask(JsonObject root) throws Exception {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("eval-language");
+        if (dc != null) {
+            String lan = root.getStringOrDefault("language", "simple");
+            boolean predicate = root.getBooleanOrDefault("predicate", false);
+            String template = root.getStringOrDefault("template", "");
+            String body = root.getStringOrDefault("body", "");
+            Map<String, String> map = new LinkedHashMap<>();
+            Collection<JsonObject> headers = root.getCollection("headers");
+            if (headers != null) {
+                map = new LinkedHashMap<>();
+                for (JsonObject jo : headers) {
+                    map.put(jo.getString("key"), jo.getString("value"));
+                }
+            }
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON,
+                    Map.of("language", lan, "predicate", predicate, 
"template", template, "body", body, "headers", map));
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionLoadTask(JsonObject root) throws Exception {
         List<String> files = root.getCollection("source");
         boolean restart = root.getBooleanOrDefault("restart", false);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index bc094d14443a..ff00446a0317 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -9,6 +9,7 @@
     { "name": "dependency", "fullName": "dependency", "description": "Displays 
all Camel dependencies required to run", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"copy", "fullName": "dependency copy", "description": "Copies all Camel 
dependencies required to run to a specific directory", "sourc [...]
     { "name": "dirty", "fullName": "dirty", "description": "Check if there are 
dirty files from previous Camel runs that did not terminate gracefully", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty", 
"options": [ { "names": "--clean", "description": "Clean dirty files which are 
no longer in use", "defaultValue": "false", "javaType": "boolean", "type": 
"boolean" }, { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", " [...]
     { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the internet [...]
+    { "name": "eval", "fullName": "eval", "description": "Evaluate Camel 
expressions and scripts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", 
"fullName": "eval expression", "description": "Evaluates Camel expression", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
     { "name": "explain", "fullName": "explain", "description": "Explain what a 
Camel route does using AI\/LLM", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": 
"--api-key", "description": "API key for authentication. Also reads 
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' 
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", "javaTyp [...]
     { "name": "export", "fullName": "export", "description": "Export to other 
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": 
"--build-property", "description": "Maven\/Gradle build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--build-tool", "description": "DEPRECATED: Build tool to use (maven 
or gradle) (gradle is deprecated)", "defaultV [...]
     { "name": "get", "fullName": "get", "description": "Get status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean", 
"fullName": "get  [...]
@@ -27,7 +28,7 @@
     { "name": "stop", "fullName": "stop", "description": "Shuts down running 
Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ { 
"names": "--kill", "description": "To force killing the process (SIGKILL)", 
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", 
"description": "Display the help and sub-commands", "javaType": "boolean", 
"type": "boolean" } ] },
     { "name": "top", "fullName": "top", "description": "Top status of Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ { 
"names": "--watch", "description": "Execute periodically and showing output 
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context", 
"fullName": "top  [...]
     { "name": "trace", "fullName": "trace", "description": "Tail message 
traces from running Camel integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options": 
[ { "names": "--action", "description": "Action to start, stop, clear, list 
status, or dump traces", "defaultValue": "status", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use 
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
-    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"message", "fullName": "transform message", "description": "Transform message 
from one format to another via an existing running Camel integration", " [...]
+    { "name": "transform", "fullName": "transform", "description": "Transform 
message or Camel routes", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"message", "fullName": "transform message", "description": "Transform message 
from one format to another via an existing running Camel integration", " [...]
     { "name": "update", "fullName": "update", "description": "Update Camel 
project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"list", "fullName": "update list", "description": "List available update 
versions for Camel and its runtime variants", "sourceClass": 
"org.apache.camel.dsl.jbang.cor [...]
     { "name": "version", "fullName": "version", "description": "Manage Camel 
versions", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.version.VersionCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "version get", "description": "Displays current Camel version", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.version.VersionGet", 
[...]
   ]
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 885d8d490dfd..d959b8267e17 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
@@ -125,6 +125,8 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("runtime", new CommandLine(new 
DependencyRuntime(main)))
                         .addSubcommand("update", new CommandLine(new 
DependencyUpdate(main))))
                 .addSubcommand("dirty", new CommandLine(new Dirty(main)))
+                .addSubcommand("eval", new CommandLine(new EvalCommand(main))
+                        .addSubcommand("expression", new CommandLine(new 
EvalExpressionCommand(main))))
                 .addSubcommand("export", new CommandLine(new Export(main)))
                 .addSubcommand("explain", new CommandLine(new Explain(main)))
                 .addSubcommand("get", new CommandLine(new CamelStatus(main))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
new file mode 100644
index 000000000000..3af4549c48d4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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 picocli.CommandLine;
+
[email protected](name = "eval",
+                     description = "Evaluate Camel expressions and scripts 
(use transform --help to see sub commands)",
+                     sortOptions = false, showDefaultValues = true)
+public class EvalCommand extends CamelCommand {
+
+    public EvalCommand(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        new CommandLine(this).execute("--help");
+        return 0;
+    }
+
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
new file mode 100644
index 000000000000..bd4815d27144
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
@@ -0,0 +1,224 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.fusesource.jansi.Ansi;
+import picocli.CommandLine;
+
[email protected](name = "expression",
+                     description = "Evaluates Camel expression", sortOptions = 
false,
+                     showDefaultValues = true)
+public class EvalExpressionCommand extends ActionWatchCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--camel-version" },
+                        description = "To run using a different Camel version 
than the default version.")
+    String camelVersion;
+
+    @CommandLine.Parameters(description = "Language to use", defaultValue = 
"simple")
+    String language;
+
+    @CommandLine.Parameters(description = "Whether to force evaluating as 
predicate", defaultValue = "false")
+    boolean predicate;
+
+    @CommandLine.Option(names = {
+            "--template" },
+                        description = "The template to use for evaluating 
(prefix with file: to refer to loading template from file)",
+                        required = true)
+    private String template;
+
+    @CommandLine.Option(names = { "--body" },
+                        description = "Message body (prefix with file: to 
refer to loading message body from file)")
+    String body;
+
+    @CommandLine.Option(names = { "--header" },
+                        description = "Message header (key=value)")
+    List<String> headers;
+
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "10000",
+                        description = "Timeout in millis waiting for 
evaluation to be done")
+    long timeout = 10000;
+
+    @CommandLine.Option(names = { "--repo", "--repos" },
+                        description = "Additional maven repositories (Use 
commas to separate multiple repositories)")
+    String repositories;
+
+    long pid;
+
+    public EvalExpressionCommand(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        if (template == null || template.isBlank()) {
+            printer().printErr("Template is required");
+            return -1;
+        }
+        if (template.startsWith("file:")) {
+            Path f = Path.of(template.substring(5));
+            if (!Files.exists(f)) {
+                printer().printErr("Template file does not exist: " + f);
+                return -1;
+            }
+        }
+
+        Integer exit;
+        List<Long> pids = findPids(name);
+        if (pids.size() == 1) {
+            this.pid = pids.get(0);
+            printer().println("Using existing running Camel integration to 
evaluate (pid: " + this.pid + ")");
+            exit = super.doCall();
+        } else {
+            try {
+                // start a new empty camel in the background
+                Run run = new Run(getMain());
+                // requires camel 4.3 onwards
+                if (camelVersion != null && VersionHelper.isLE(camelVersion, 
"4.2.0")) {
+                    printer().printErr("This requires Camel version 4.3 or 
newer");
+                    return -1;
+                }
+                exit = run.runTransformMessage(camelVersion, repositories);
+                this.pid = run.spawnPid;
+                if (exit == 0) {
+                    exit = super.doCall();
+                }
+            } finally {
+                if (pid > 0) {
+                    // cleanup output file
+                    Path outputFile = getOutputFile(Long.toString(pid));
+                    PathUtils.deleteFile(outputFile);
+                    // stop running camel as we are done
+                    Path parent = CommandLineHelper.getCamelDir();
+                    Path pidFile = parent.resolve(Long.toString(pid));
+                    if (Files.exists(pidFile)) {
+                        PathUtils.deleteFile(pidFile);
+                    }
+                }
+            }
+        }
+
+        return exit;
+    }
+
+    @Override
+    protected Integer doWatchCall() throws Exception {
+        JsonObject root = new JsonObject();
+        root.put("action", "eval");
+        root.put("language", language);
+        root.put("predicate", predicate);
+        root.put("template", Jsoner.escape(template));
+        if (body != null) {
+            root.put("body", Jsoner.escape(body));
+        }
+        if (headers != null) {
+            JsonArray arr = new JsonArray();
+            for (String h : headers) {
+                JsonObject jo = new JsonObject();
+                if (!h.contains("=")) {
+                    printer().println("Header must be in key=value format, 
was: " + h);
+                    return 0;
+                }
+                jo.put("key", StringHelper.before(h, "="));
+                jo.put("value", StringHelper.after(h, "="));
+                arr.add(jo);
+            }
+            root.put("headers", arr);
+        }
+
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        Path f = getActionFile(Long.toString(pid));
+        try {
+            PathUtils.writeTextSafely(root.toJson(), f);
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            String status = jo.getString("status");
+            if ("success".equals(status)) {
+                String result = jo.getString("result");
+                printer().println(result);
+            } else {
+                JsonObject cause = jo.getMap("exception");
+                if (cause != null) {
+                    String msg = cause.getString("message");
+                    if (msg != null) {
+                        msg = Jsoner.unescape(msg);
+                    }
+                    String st = cause.getString("stackTrace");
+                    if (st != null) {
+                        st = Jsoner.unescape(st);
+                    }
+                    if (msg != null) {
+                        String text = 
Ansi.ansi().fgRed().a(msg).reset().toString();
+                        printer().printErr(text);
+                        printer().println();
+                    }
+                    if (st != null) {
+                        String text = 
Ansi.ansi().fgRed().a(st).reset().toString();
+                        printer().printErr(text);
+                        printer().println();
+                    }
+                    return 1;
+                }
+            }
+        }
+
+        return 0;
+    }
+
+    protected JsonObject waitForOutputFile(Path outputFile) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < timeout) {
+            try {
+                // give time for response to be ready
+                Thread.sleep(20);
+
+                if (Files.exists(outputFile)) {
+                    String text = Files.readString(outputFile);
+                    return (JsonObject) Jsoner.deserialize(text);
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return null;
+    }
+
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
index 274af9d325c2..081fcbf4dec3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
@@ -76,7 +76,7 @@ public class TransformMessageAction extends 
ActionWatchCommand {
 
     @CommandLine.Option(names = {
             "--template" },
-                        description = "The template to use for message 
transformation (prefix with file: to refer to loading message body from file)")
+                        description = "The template to use for message 
transformation (prefix with file: to refer to loading template from file)")
     private String template;
 
     @CommandLine.Option(names = { "--option" },
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
new file mode 100644
index 000000000000..0e9554553cd3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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 org.apache.camel.dsl.jbang.core.commands.action.EvalExpressionCommand;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+class EvalSimpleTest extends CamelCommandBaseTestSupport {
+
+    @Test
+    public void shouldEvalSimple() throws Exception {
+        String[] args = new String[] { "--template=${length()}", 
"--body=hello_world" };
+        EvalExpressionCommand command = createCommand(args);
+        int exit = command.doCall();
+        Assertions.assertEquals(0, exit);
+
+        var lines = printer.getLines();
+        Assertions.assertNotNull(lines);
+        Assertions.assertEquals(2, lines.size());
+        Assertions.assertEquals("11", lines.get(1));
+    }
+
+    private EvalExpressionCommand createCommand(String... args) {
+        EvalExpressionCommand command = new EvalExpressionCommand(new 
CamelJBangMain().withPrinter(printer));
+        if (args != null) {
+            CommandLine.populateCommand(command, args);
+        }
+        return command;
+    }
+
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
index 83490a515a7b..0155c7ef70f2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
@@ -34,7 +34,7 @@ class ParameterExceptionHandlerTest {
         Assertions.assertEquals(5, lines.length, "5 lines for the error is 
expected but received " + lines.length);
         Assertions.assertEquals("Unmatched argument at index 0: 
'firstInvalid'", lines[0],
                 "First line mentioning unmatched argument");
-        Assertions.assertEquals("Did you mean: camel stop or camel infra or 
camel plugin?", lines[1],
+        Assertions.assertEquals("Did you mean: camel eval or camel stop or 
camel infra?", lines[1],
                 "Second line with suggestion in case it is a typo");
         Assertions.assertEquals(
                 "Maybe a specific Camel JBang plugin must be installed? (Try 
camel plugin --help' for more information)",


Reply via email to