diff --git a/API.md b/API.md index 682d89e..1d0b758 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 @@ -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` @@ -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). -

Source

+

Source

## `format-command-error` ``` clojure @@ -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). -

Source

+

Source

## `format-command-help` ``` clojure @@ -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:`. -

Source

+

Source

## `format-opts` ``` clojure @@ -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. -

Source

+

Source

## `validate-opts` ``` clojure diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index 89052f0..a55ee1b 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -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] @@ -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 @@ -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` diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 39cfc45..9bad381 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -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]