Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1713-L1741">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1735-L1763">Source</a></sub></p>

## <a name="babashka.cli/apply-defaults">`apply-defaults`</a>
``` clojure
Expand Down Expand Up @@ -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"}}
Expand Down Expand Up @@ -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).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2046-L2142">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2068-L2165">Source</a></sub></p>

## <a name="babashka.cli/format-command-error">`format-command-error`</a>
``` clojure
Expand All @@ -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).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1752-L1799">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1774-L1821">Source</a></sub></p>

## <a name="babashka.cli/format-command-help">`format-command-help`</a>
``` clojure
Expand Down Expand Up @@ -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:`.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1670-L1711">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1692-L1733">Source</a></sub></p>

## <a name="babashka.cli/format-opts">`format-opts`</a>
``` clojure
Expand Down Expand Up @@ -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.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1189-L1211">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1211-L1233">Source</a></sub></p>

## <a name="babashka.cli/validate-opts">`validate-opts`</a>
``` clojure
Expand Down
33 changes: 29 additions & 4 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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\"}}
Expand Down
23 changes: 23 additions & 0 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading