diff --git a/API.md b/API.md index 1d0b7581..d4c4fdfe 100644 --- a/API.md +++ b/API.md @@ -67,7 +67,7 @@ Terminates the process after `dispatch`'s `:help` option prints an *error* Must exit or throw. Default: `System/exit` (JVM), `js/process.exit` (Node), `throw` (browser). -

Source

+

Source

## `apply-defaults` ``` clojure @@ -166,7 +166,8 @@ Command dispatcher. Instead of a table, bb.cli also accepts a tree-shaped format: a map node with the root options and a `:cmd` map from command name to child node. Each node - takes the same keys a table entry does (except `:cmds`): + takes the same keys a table entry does (except `:cmds`). Command names may be + strings or symbols; symbols are stringified. ```clojure {:spec {:format {:desc "edn or table"}} @@ -223,7 +224,7 @@ Command dispatcher. Each entry in the table may have additional [`parse-args`](#babashka.cli/parse-args) options. For more information and examples, see [README.md](README.md#commands). -

Source

+

Source

## `format-command-error` ``` clojure @@ -247,7 +248,7 @@ Render a terse, helpful message (a string) for a dispatch error. this, then calls [`*exit-fn*`](#babashka.cli/*exit-fn*)). Call it from a custom `:error-fn` to keep the standard message and add your own output. `--help`/`-h` is not an error - it goes to the `:help-fn`, rendered by [`format-command-help`](#babashka.cli/format-command-help). -

Source

+

Source

## `format-command-help` ``` clojure @@ -292,7 +293,7 @@ Render conventional `--help` text (a string) for the command at path `cmds` This is the renderer the `:help` option uses; call it from a custom `:help-fn` to render the standard help and then add your own output. An entry may carry `:no-doc true` to be omitted from `Commands:`. -

Source

+

Source

## `format-opts` ``` clojure @@ -459,7 +460,7 @@ Converts a `dispatch` table into a tree. Each `:cmds` becomes a path of ``` A tree passed in is normalized and returned, so the function is idempotent. -

Source

+

Source

## `validate-opts` ``` clojure diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index a55ee1b4..8eb8539d 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1159,19 +1159,43 @@ node)) node))) +(defn- cmd-name + "Command names are strings. A symbol key (nicer in bb, where the task is + already a symbol) is stringified; other names are left as is." + [c] + (if (symbol? c) (str c) c)) + +(defn- stringify-cmds + "Stringify symbol command names in `:cmd` keys and `:cmd-order` / `::cmd-order` + entries. Returns `node` unchanged (identical) when there is nothing to do. + Symbol command names are a bb/JVM convenience, so skip this for squint (its + `symbol?` is true for strings) and cljd (which lacks a runtime `symbol?`); + both use string keys." + [node] + (if #?(:squint false + :cljd false + :default (or (some symbol? (keys (:cmd node))) + (some symbol? (::cmd-order node)) + (some symbol? (:cmd-order node)))) + (cond-> node + (:cmd node) (update :cmd #(into {} (map (fn [[k v]] [(cmd-name k) v])) %)) + (::cmd-order node) (update ::cmd-order #(mapv cmd-name %)) + (:cmd-order node) (update :cmd-order #(mapv cmd-name %))) + node)) + (defn- normalize-node "Normalize tree `node`, recursively. Rejects table-entry `:cmds` on a node, dedupes an explicit `:cmd-order` and reconciles the recorded `::cmd-order` with the actual `:cmd` children - stale names dropped, unrecorded children appended in `:cmd` map order (whatever order the map iterates in is at least stable from then on). - Folds a var `:fn` / `:exec-fn`'s spec and docstring into the node. - Idempotent." + Folds a var `:fn` / `:exec-fn`'s spec and docstring into the node. Symbol + command names are stringified. Idempotent." [node] (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}))) - (let [node (enrich-from-var node)] + (let [node (-> node enrich-from-var stringify-cmds)] (if-let [m (:cmd node)] (let [recorded (into [] (comp (distinct) (filter #(contains? m %))) (::cmd-order node)) order (into recorded (remove (set recorded)) (keys m)) @@ -2059,7 +2083,8 @@ $env.config.completions.external.completer = {|spans| Instead of a table, bb.cli also accepts a tree-shaped format: a map node with the root options and a `:cmd` map from command name to child node. Each node - takes the same keys a table entry does (except `:cmds`): + takes the same keys a table entry does (except `:cmds`). Command names may be + strings or symbols; symbols are stringified. ```clojure {:spec {:format {:desc \"edn or table\"}} diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 9bad3819..205cdb60 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -612,6 +612,29 @@ (is (= (cli/format-command-help {:table table :cmds ["dev"] :prog "tool"}) (cli/format-command-help {:table tree :cmds ["dev"] :prog "tool"}))))))) +;; symbol command names are a bb/JVM convenience; squint's symbol? is true for +;; strings and cljd has no runtime symbol?, so both keep string keys and skip this +#?(:squint nil :cljd nil :default + (deftest dispatch-symbol-cmd-test + (testing "symbol :cmd keys are stringified and dispatch like string keys" + (let [tree {:cmd {'lock {:fn identity} 'unlock {:fn identity}}}] + (is (= ["lock" "unlock"] (keys (:cmd (cli/table->tree tree))))) + (is (submap? {:dispatch ["lock"] :opts {:force true}} + (cli/dispatch tree ["lock" "--force"]))) + (is (str/includes? (cli/format-command-help {:table tree :prog "t"}) "lock")))) + (testing "symbol :cmds in a table are stringified too" + (is (submap? {:dispatch ["add"]} + (cli/dispatch [{:cmds ['add] :fn identity} {:cmds [] :fn identity}] + ["add"])))) + (testing "a symbol :cmd-order matches the stringified keys" + (let [tree {:cmd-order ['b 'a] :cmd {'a {:fn identity} 'b {:fn identity}}}] + (is (= ["b" "a"] + (->> (str/split-lines (cli/format-command-help {:table tree :prog "t"})) + (drop-while #(not= "Commands:" %)) + rest + (take-while #(str/starts-with? % " ")) + (mapv #(str/trim %))))))))) + (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}