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
63 changes: 56 additions & 7 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

-----
# <a name="babashka.cli">babashka.cli</a>
Expand Down Expand Up @@ -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).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1696-L1724">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L1697-L1725">Source</a></sub></p>

## <a name="babashka.cli/apply-defaults">`apply-defaults`</a>
``` clojure
Expand Down Expand Up @@ -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.
Expand All @@ -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).
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2025-L2113">Source</a></sub></p>
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli.cljc#L2030-L2122">Source</a></sub></p>

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

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

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

## <a name="babashka.cli/validate-opts">`validate-opts`</a>
``` clojure
Expand Down Expand Up @@ -504,3 +514,42 @@ Main entrypoint for command line usage.
```
Function.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/babashka/cli/exec.clj#L92-L95">Source</a></sub></p>

-----
# <a name="scratch">scratch</a>






## <a name="scratch/-main">`-main`</a>
``` clojure
(-main & args)
```
Function.
<p><sub><a href="https://github.com/babashka/cli/blob/main/src/scratch.clj#L15-L17">Source</a></sub></p>

## <a name="scratch/dns-get-spec">`dns-get-spec`</a>



<p><sub><a href="https://github.com/babashka/cli/blob/main/src/scratch.clj#L8-L8">Source</a></sub></p>

## <a name="scratch/dns-spec">`dns-spec`</a>



<p><sub><a href="https://github.com/babashka/cli/blob/main/src/scratch.clj#L7-L7">Source</a></sub></p>

## <a name="scratch/global-spec">`global-spec`</a>



<p><sub><a href="https://github.com/babashka/cli/blob/main/src/scratch.clj#L4-L6">Source</a></sub></p>

## <a name="scratch/table">`table`</a>



<p><sub><a href="https://github.com/babashka/cli/blob/main/src/scratch.clj#L10-L13">Source</a></sub></p>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 16 additions & 7 deletions src/babashka/cli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1061,8 +1061,9 @@
;; a runnable command: show labeled positionals from :args->opts, if
;; any. We don't show a generic `[<args>]` 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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions test/babashka/cli_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading