diff --git a/API.md b/API.md index 8881275..682d89e 100644 --- a/API.md +++ b/API.md @@ -24,6 +24,12 @@ - [`babashka.cli.exec`](#babashka.cli.exec) - [`-main`](#babashka.cli.exec/-main) - Main entrypoint for command line usage. - [`main`](#babashka.cli.exec/main) +- [`scratch`](#scratch) + - [`-main`](#scratch/-main) + - [`dns-get-spec`](#scratch/dns-get-spec) + - [`dns-spec`](#scratch/dns-spec) + - [`global-spec`](#scratch/global-spec) + - [`table`](#scratch/table) ----- # babashka.cli @@ -61,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 @@ -182,13 +188,17 @@ Command dispatcher. * `:args` - concatenation of unparsed commands and args * `:rest-cmds`: DEPRECATED, this will be removed in a future version + A node may use `:exec-fn` instead of `:fn` as a convenience: it is called with + just the parsed `:opts` map rather than the whole dispatch result. `:exec-fn` + wins if a node has both. + 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` - is `[]`: + is `[]` (or just a single node map). `:exec-fn` hands `f` the parsed opts: ```clojure - (dispatch [{:cmds [] :fn f :spec spec}] args {:prog "tool" :help true}) + (dispatch [{:cmds [] :exec-fn f :spec spec}] args {:prog "tool" :help true}) ``` Provide an `:error-fn` to deal with non-matches. @@ -209,7 +219,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 @@ -233,7 +243,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 @@ -278,7 +288,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 @@ -445,7 +455,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 @@ -504,3 +514,42 @@ Main entrypoint for command line usage. ``` Function.

Source

+ +----- +# scratch + + + + + + +## `-main` +``` clojure +(-main & args) +``` +Function. +

Source

+ +## `dns-get-spec` + + + +

Source

+ +## `dns-spec` + + + +

Source

+ +## `global-spec` + + + +

Source

+ +## `table` + + + +

Source

diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bcc1d..c79ff1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ For breaking changes, check [here](#breaking-changes). - [#174](https://github.com/babashka/cli/issues/174): `:edn` `:coerce` option must provide an explicit value ([@lread](https://github.com/lread)) +- Introduce `:exec-fn` option in dispatch ## v0.12.75 (2026-06-25) diff --git a/README.md b/README.md index c4f5f39..b64b3f8 100644 --- a/README.md +++ b/README.md @@ -137,15 +137,19 @@ stand-in for `git`. Save it to `mygit.clj`. :bare {:coerce :boolean ; defines a boolean flag :desc "Create a bare repository"}}) -(defn run [{:keys [opts]}] +(defn run [opts] (println "Here are your cli args!:" opts)) (defn -main [& args] - (cli/dispatch {:fn run :spec spec} args {:prog "mygit" :help true})) + (cli/dispatch {:exec-fn run :spec spec} args {:prog "mygit" :help true})) (apply -main *command-line-args*) ``` +An `:exec-fn` is called with the parsed options map, which is usually all +you need. Use `:fn` instead when you want the whole dispatch result +(`{:opts ... :dispatch ... :args ...}`). + The `:help true` option supplied to `dispatch` wires up automatic `--help`/`-h` support and terse error messages (as opposed to thrown exceptions) for you. Let's request usage help: @@ -198,7 +202,7 @@ To add commands to this CLI, we need to specify a command structure. We'll just Alter `mygit.clj`: ``` clojure ;; renamed from `run` -(defn clone [{:keys [opts]}] +(defn clone [opts] (println "Here are your cli args!:" opts)) ;; new @@ -207,7 +211,7 @@ Alter `mygit.clj`: ;; new (def tree - {:cmd {"clone" {:fn clone :doc "Clone a repository" :spec spec} + {:cmd {"clone" {:exec-fn clone :doc "Clone a repository" :spec spec} "version" {:fn version :doc "Print version"}}}) ;; updated to use `tree` command structure diff --git a/src/babashka/cli.cljc b/src/babashka/cli.cljc index e7a50b0..89052f0 100644 --- a/src/babashka/cli.cljc +++ b/src/babashka/cli.cljc @@ -1061,8 +1061,9 @@ ;; a runnable command: show labeled positionals from :args->opts, if ;; any. We don't show a generic `[]` placeholder otherwise ;; (matches argparse/clap/click/picocli/cli-tools). - (:fn node) (when-let [labels (args->opts-labels (:args->opts node))] - (str " " (str/join " " labels))) + (or (:fn node) + (:exec-fn node)) (when-let [labels (args->opts-labels (:args->opts node))] + (str " " (str/join " " labels))) :else ""))) (defn- help-commands-table [node] @@ -1871,10 +1872,11 @@ $env.config.completions.external.completer = {|spans| (if-let [subcmd-info (get (:cmd cmd-info) arg)] (recur (conj cmds arg) all-opts rest subcmd-info (merge inherited (inherited-entries (:spec kwm) inherit-opt))) - (if (:fn cmd-info) + (if (or (:fn cmd-info) (:exec-fn cmd-info)) {:cmd-info cmd-info :dispatch cmds - :opts (dissoc all-opts ::opts-by-cmds) + :opts (vary-meta (dissoc all-opts ::opts-by-cmds) + update :org.babashka/cli merge {:dispatch cmds :args args}) :args args} (if arg {:error :no-match @@ -1922,7 +1924,10 @@ $env.config.completions.external.completer = {|spans| :tree tree} (select-keys res [:wrong-input :opts :dispatch])) opts)) - nil ((:fn cmd-info) (dissoc res :cmd-info))))))) + nil (let [res (dissoc res :cmd-info)] + (if-let [exec-fn (:exec-fn cmd-info)] + (exec-fn (:opts res)) + ((:fn cmd-info) res)))))))) (defn- node-with-help "Give one node a `--help`/`-h` option: add `:help` to its `:spec` (so it parses @@ -2060,13 +2065,17 @@ $env.config.completions.external.completer = {|spans| * `:args` - concatenation of unparsed commands and args * `:rest-cmds`: DEPRECATED, this will be removed in a future version + A node may use `:exec-fn` instead of `:fn` as a convenience: it is called with + just the parsed `:opts` map rather than the whole dispatch result. `:exec-fn` + wins if a node has both. + 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` - is `[]`: + is `[]` (or just a single node map). `:exec-fn` hands `f` the parsed opts: ```clojure - (dispatch [{:cmds [] :fn f :spec spec}] args {:prog \"tool\" :help true}) + (dispatch [{:cmds [] :exec-fn f :spec spec}] args {:prog \"tool\" :help true}) ``` Provide an `:error-fn` to deal with non-matches. diff --git a/test/babashka/cli_test.cljc b/test/babashka/cli_test.cljc index 95a2cf4..39cfc45 100644 --- a/test/babashka/cli_test.cljc +++ b/test/babashka/cli_test.cljc @@ -612,6 +612,35 @@ (is (= (cli/format-command-help {:table table :cmds ["dev"] :prog "tool"}) (cli/format-command-help {:table tree :cmds ["dev"] :prog "tool"}))))))) +(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} + (cli/dispatch {:exec-fn identity} ["--foo" "1" "--bar"]))) + (is (= [:args :dispatch :opts] + (sort (keys (cli/dispatch {:fn identity} ["--foo" "1"])))))) + (testing ":exec-fn works as a subcommand and honors its :spec" + (is (= {:x 2} + (cli/dispatch {:cmd {"add" {:exec-fn identity :spec {:x {:coerce :long}}}}} + ["add" "--x" "2"])))) + (testing "--help on an :exec-fn node renders its spec, does not call the fn" + (let [called (atom false) + out (with-out-str + (cli/dispatch {:cmd {"add" {:exec-fn (fn [_] (reset! called true)) + :spec {:x {:desc "the x"}}}}} + ["add" "--help"] {:prog "t" :help true}))] + (is (str/includes? out "Usage: t add")) + (is (str/includes? out "the x")) + (is (false? @called)))) + (testing ":exec-fn wins if a node has both" + (is (= {:foo true} + (cli/dispatch {:exec-fn identity :fn (fn [_] :never)} ["--foo"])))) + (testing "the opts map carries the dispatch path and leftover args on its meta" + (let [opts (cli/dispatch {:cmd {"add" {:exec-fn identity + :args->opts [:y]}}} + ["add" "5" "extra"])] + (is (= {:dispatch ["add"] :args ["extra"]} + (:org.babashka/cli (meta opts))))))) + (defn- listed-command-names "Command names from the `Commands:` section of help/error output, in order." [s]