From 9ca6a21888e2e57e04ec2fecdb64deda53ba4456 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 23 Jul 2026 14:05:34 +0200 Subject: [PATCH 1/2] Reject a :cmd key that is not a string, symbol or keyword --- src/babashka/cli.cljc | 10 ++++++++++ test/babashka/cli_test.cljc | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 68afc2c..31c6aab 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1291,6 +1291,16 @@ (when (:cmds node) (throw (ex-info "A tree node contains :cmds (table entry syntax): nest children under :cmd, or pass a table (vector of entries)" {:node node}))) + ;; a command key must name a command. A tree built in an evaluated context + ;; (e.g. unquoted function metadata) can turn a symbol key into the function + ;; value, which would become a garbage command name and silently misroute + #?(:squint nil :cljd nil :default + (doseq [k (if (vector? (:cmd node)) (map first (:cmd node)) (keys (:cmd node)))] + (when-not (or (string? k) (symbol? k) (keyword? k)) + (throw (ex-info (str "Command name " (pr-str k) " is not a string, symbol or keyword" + " (tree built in an evaluated context, e.g. function metadata?" + " quote it or use string keys)") + {:node node :key k}))))) (let [;; a vector `:cmd` (pairs `[[name child] ...]`) preserves child order without ;; relying on map iteration order; fold it into the lookup map + `::cmd-order` node (if (vector? (:cmd node)) diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 5793bc8..81569ec 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -781,6 +781,13 @@ (take-while #(str/starts-with? % " ")) (mapv #(str/trim %))))))))) +#?(:squint nil :cljd nil :default + (deftest cmd-key-guard-test + (testing "a :cmd key that is not a name throws with an explanation" + (is (thrown-with-msg? + Exception #"is not a string, symbol or keyword" + (cli/dispatch {:cmd {identity {:fn identity}}} ["x"])))))) + (deftest dispatch-exec-fn-test (testing ":exec-fn is called with just the parsed opts; :fn with the whole map" (is (= {:foo 1 :bar true} From 0659c64a455a8e09aef5780ee06897d85cdb8168 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 23 Jul 2026 14:08:00 +0200 Subject: [PATCH 2/2] Shorten error --- src/babashka/cli.cljc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 31c6aab..05451ff 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1291,15 +1291,10 @@ (when (:cmds node) (throw (ex-info "A tree node contains :cmds (table entry syntax): nest children under :cmd, or pass a table (vector of entries)" {:node node}))) - ;; a command key must name a command. A tree built in an evaluated context - ;; (e.g. unquoted function metadata) can turn a symbol key into the function - ;; value, which would become a garbage command name and silently misroute #?(:squint nil :cljd nil :default (doseq [k (if (vector? (:cmd node)) (map first (:cmd node)) (keys (:cmd node)))] (when-not (or (string? k) (symbol? k) (keyword? k)) - (throw (ex-info (str "Command name " (pr-str k) " is not a string, symbol or keyword" - " (tree built in an evaluated context, e.g. function metadata?" - " quote it or use string keys)") + (throw (ex-info (str "Command name " (pr-str k) " is not a string, symbol or keyword") {:node node :key k}))))) (let [;; a vector `:cmd` (pairs `[[name child] ...]`) preserves child order without ;; relying on map iteration order; fold it into the lookup map + `::cmd-order`