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
14 changes: 9 additions & 5 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#L1697-L1725">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1713-L1741">Source</a></sub></p>

## <a name="babashka.cli/apply-defaults">`apply-defaults`</a>
``` clojure
Expand Down Expand Up @@ -192,6 +192,10 @@ Command dispatcher.
just the parsed `:opts` map rather than the whole dispatch result. `:exec-fn`
wins if a node has both.

When `:fn` / `:exec-fn` is a var, its `:org.babashka/cli` metadata (`:spec`,
`:args->opts`, ...) and its docstring (as `:doc`) are folded into the node.
Explicit node keys win.

Use an empty `:cmds` vector to always match or to provide global options.

For a single-command CLI (no commands), use a one-entry table whose `:cmds`
Expand Down Expand Up @@ -219,7 +223,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#L2030-L2122">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2046-L2142">Source</a></sub></p>

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

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

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

## <a name="babashka.cli/validate-opts">`validate-opts`</a>
``` clojure
Expand Down
28 changes: 24 additions & 4 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1144,19 +1144,35 @@
extra (assoc :cmd (merge (:cmd node) extra))
extra (update ::cmd-order (fnil into []) (keys extra))))))

(defn- enrich-from-var
"When a node's `:fn` / `:exec-fn` is a var, fold in what the var already
declares: its `:org.babashka/cli` metadata (`:spec`, `:args->opts`, ...) and,
when the node has no `:doc`, its docstring. Explicit node keys win. A plain fn
value or symbol is left as is."
[node]
(let [fv (or (:fn node) (:exec-fn node))]
(if #?(:cljd false :default (var? fv))
(let [m (meta fv)
node (merge (:org.babashka/cli m) node)]
(if (and (:doc m) (not (:doc node)))
(assoc node :doc (:doc m))
node))
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).
Idempotent; an already-normalized node comes back `identical?` (subtrees
are shared, not copied)."
Folds a var `:fn` / `:exec-fn`'s spec and docstring into the node.
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})))
(if-let [m (:cmd node)]
(let [node (enrich-from-var node)]
(if-let [m (:cmd node)]
(let [recorded (into [] (comp (distinct) (filter #(contains? m %))) (::cmd-order node))
order (into recorded (remove (set recorded)) (keys m))
m' (reduce-kv (fn [acc k v]
Expand All @@ -1168,7 +1184,7 @@
(not (identical? m m')) (assoc :cmd m')
(not= order (::cmd-order node)) (assoc ::cmd-order order)
(and deduped (not= deduped (:cmd-order node))) (assoc :cmd-order deduped)))
node))
node)))

(defn table->tree
"Converts a `dispatch` table into a tree. Each `:cmds` becomes a path of
Expand Down Expand Up @@ -2069,6 +2085,10 @@ $env.config.completions.external.completer = {|spans|
just the parsed `:opts` map rather than the whole dispatch result. `:exec-fn`
wins if a node has both.

When `:fn` / `:exec-fn` is a var, its `:org.babashka/cli` metadata (`:spec`,
`:args->opts`, ...) and its docstring (as `:doc`) are folded into the node.
Explicit node keys win.

Use an empty `:cmds` vector to always match or to provide global options.

For a single-command CLI (no commands), use a one-entry table whose `:cmds`
Expand Down
30 changes: 30 additions & 0 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,36 @@
(is (= {:dispatch ["add"] :args ["extra"]}
(:org.babashka/cli (meta opts)))))))

;; vars: squint has none, cljd has no var? predicate, so the #'a-command
;; literal below is clj/cljs only
#?(:squint nil :cljd nil :default
(defn a-command
"Does a thing"
{:org.babashka/cli {:spec {:force {:coerce :boolean :desc "Force it"}}}}
[opts]
(assoc opts :ran :a-command)))

#?(:squint nil :cljd nil :default
(deftest dispatch-var-fn-test
(testing "a var :fn / :exec-fn contributes its spec and docstring to the node"
(let [tree {:cmd {"do" {:exec-fn #'a-command}}}]
(testing "spec from the var's :org.babashka/cli meta drives parsing"
(is (= {:force true :ran :a-command}
(cli/dispatch tree ["do" "--force"]))))
(testing "--help shows the option from the var spec and the docstring"
(let [help (with-out-str (cli/dispatch tree ["do" "--help"] {:prog "t" :help true}))]
(is (str/includes? help "Does a thing"))
(is (str/includes? help "Force it"))))
(testing "the command listing uses the var's docstring"
(let [help (with-out-str (cli/dispatch tree ["--help"] {:prog "t" :help true}))]
(is (str/includes? help "Does a thing"))))))
(testing "an explicit node :doc wins over the var docstring"
(let [help (with-out-str
(cli/dispatch {:cmd {"do" {:exec-fn #'a-command :doc "Custom"}}}
["--help"] {:prog "t" :help true}))]
(is (str/includes? help "Custom"))
(is (not (str/includes? help "Does a thing")))))))

(defn- listed-command-names
"Command names from the `Commands:` section of help/error output, in order."
[s]
Expand Down
Loading