babashka/cli(opens in a new tab)
Language: Clojure
Turn Clojure functions into CLIs!
- stars
- 295
- watchers
- 2
- forks
- 22
- open issues
- 1
- Last updated
- 10/27/2026
Language: Clojure
Turn Clojure functions into CLIs!
Turn Clojure functions into Command Line Interfaces! This library can be used from:
For Clojure, ClojureScript, and ClojureDart include a :deps entry in your deps.edn file:
org.babashka/cli {:mvn/version "<latest-version>"}For babashka, no changes are needed; org.babashka/cli is a babashka built-in library.
For JavaScript, install from NPM:
npm install @babashka/cli
See JavaScript for how to use this library in JS.
Turn a Clojure function into a CLI that takes Unix-style command line arguments. E.g.:
$ program command --verbose --long-opt1 v1 -o v2 argWhere:
program is your executable program or script (other libraries might call this "command")command is a single or multi-word command for your program (other libraries might call this "subcommand")--verbose is a boolean option--long-opt1 v1 is an option-o v2 is a short optionarg is a positional argumentSee Terminology for precise definitions.
The main ideas:
-X exec style invocations. For lazy people like me! If you are not
familiar with clj -X, read the docs
here.--dir foo instead of :dir '"foo"' (or
who knows how to write the latter in Windows' cmd.exe or Powershell?).See clojure CLI for how to turn your -X exec functions into CLIs.
$ program command --verbose --long-opt1 v1 -o v2 arg| Term | Meaning |
|---|---|
program |
The executable program, typically launched in a terminal |
command |
A single or multi-word command for the program. Other libraries often call this a subcommand |
option |
A named option, written --opt val or short alias -o val |
flag |
The literal --opt part of --opt val, -o in -o v2, or :opt as the user passed it. We use the word flag both for boolean and non-boolean options. |
alias |
A single character alias for the option name |
argument |
A positional argument |
Note
To explain terminology choices, a concrete example:
git remote show
Some CLI libraries call git the command, and remote show the subcommand.
These same libraries typically use the term command when describing remote show in usage help.
To keep terminology consistent for CLI users and CLI developers, we avoid subcommand entirely.
We use program for git and command for remote show.
Here is an example babashka script to get you started. We'll write a small
stand-in for git. Save it to mygit.clj.
#!/usr/bin/env bb (require '[babashka.cli :as cli] '[babashka.fs :as fs]) (defn dir-exists? [path] (fs/directory? path)) (def spec {:depth {:coerce :long :alias :d ; adds -d alias for --depth :desc "Number of commits to fetch" :validate pos? ; tests if supplied --depth > 0 :require true} ; --depth,-d is required :dir {:alias :C ; like git's own -C :desc "Run as if git was started in <dir>" :validate ; tests if --dir exists, {:pred dir-exists? ; with a custom error message :ex-msg (fn [{:keys [value]}] (str "Directory does not exist: " value))}} :bare {:coerce :boolean ; defines a boolean flag :desc "Create a bare repository"}}) (defn run [opts] (println "Here are your cli args!:" opts)) (defn -main [& args] (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:
$ bb mygit.clj --help
Usage: mygit [options]
Options:
-d, --depth Number of commits to fetch (required)
-C, --dir Run as if git was started in <dir>
--bare Create a bare repository
-h, --help Show this help
See Commands > Help to customize help.
Let's try running with some options:
$ bb mygit.clj --depth 1 --dir my_dir --bare
Error: Directory does not exist: my_dir
Usage: mygit [options]
Run "mygit --help" for more information.
The directory was validated with dir-exists?. Because the directory does not exist, you see the custom error message produced by the :ex-msg function.
Let's create my_dir, then try again:
$ mkdir my_dir
$ bb mygit.clj --depth 1 --dir my_dir --bare
Here are your cli args!: {:depth 1, :dir my_dir, :bare true}
All validations passed, and the run function was invoked.
The :depth option includes :require true, let's see what happens when we don't include it on the command line:
$ bb mygit.clj
Error: Required option: --depth
Usage: mygit [options]
Run "mygit --help" for more information.
We, appropriately, get a terse error message and an exit status of 1.
To add commands to this CLI, we need to specify a command structure. We'll just give an example here. See Commands for more info.
Alter mygit.clj:
;; renamed from `run` (defn clone [opts] (println "Here are your cli args!:" opts)) ;; new (defn version [_] (println "mygit 1.0")) ;; new (def tree {:cmd {"clone" {:exec-fn clone :doc "Clone a repository" :spec spec} "version" {:fn version :doc "Print version"}}}) ;; updated to use `tree` command structure (defn -main [& args] (cli/dispatch tree args {:prog "mygit" :help true}))
--help now lists the available commands:
$ bb mygit.clj --help
Usage: mygit [options] <command>
Commands:
clone Clone a repository
version Print version
Options:
-h, --help Show this help
Run "mygit <command> --help" for more information on a command.
The clone command calls the clone function:
$ bb mygit.clj clone --depth 1 --bare
Here are your cli args!: {:depth 1, :bare true}
The version command calls the version function:
$ bb mygit.clj version
mygit 1.0
See Commands for shared options, inheritance, and help customization.
If you'd like to parse options yourself (instead of using dispatch),
use either lower-level parse-opts or parse-args. We will
use these parse functions in this section to demonstrate how options parsing works.
On the command line, a named option is written as --opt val or short-form alias -o val.
Options are configured with a spec (short for "options specification", not
clojure.spec): a map keyed by option name, each value a map of :coerce,
:alias, :validate, :require, :desc, etc., passed under :spec:
{:spec {:port {:coerce :long :alias :p}}}A terser shape is also supported, where each key is lifted to the top level and
keyed by option name: {:coerce {:port :long} :alias {:p :port}}. It is handy
for quick scripts and partial parsing, but only a spec can carry :desc/:ref,
so generated help and option printing need a spec. The two are otherwise
equivalent. The examples below use the spec shape.
Examples:
Parse {:port 1339} from command line arguments:
(require '[babashka.cli :as cli]) (cli/parse-opts ["--port" "1339"] {:spec {:port {:coerce :long}}}) ;;=> {:port 1339}
Use an alias (short option):
(cli/parse-opts ["-p" "1339"] {:spec {:port {:coerce :long :alias :p}}}) ;; {:port 1339}
Coerce values into a collection:
(cli/parse-opts ["--paths" "src" "--paths" "test"] {:spec {:paths {:coerce []}}}) ;;=> {:paths ["src" "test"]} (cli/parse-opts ["--paths" "src" "test"] {:spec {:paths {:coerce []}}}) ;;=> {:paths ["src" "test"]}
Transforming into a collection of a certain type:
(cli/parse-opts ["--foo" "bar" "--foo" "baz"] {:spec {:foo {:coerce [:keyword]}}}) ;; => {:foo [:bar :baz]}
In addition to the built-in coercion keywords, :coerce accepts any function (called
with the option's value as a string):
(cli/parse-opts ["--letter" "alpha"] {:spec {:letter {:coerce (fn [s] (subs s 0 1))}}}) ;;=> {:letter "a"}
Boolean flags are assumed by default, like so:
(cli/parse-opts ["--verbose"]) ;;=> {:verbose true} (cli/parse-opts ["-v" "-v" "-v"] {:spec {:verbose {:alias :v :coerce []}}}) ;;=> {:verbose [true true true]}
But you can explicitly specify :boolean coercion (and will sometimes need to, see Arguments):
(cli/parse-opts ["--verbose"] {:spec {:verbose {:coerce :boolean}}}) ;;=> {:verbose true} (cli/parse-opts ["-v" "-v" "-v"] {:spec {:verbose {:alias :v :coerce [:boolean]}}}) ;;=> {:verbose [true true true]}
Long options also support the syntax --foo=bar:
(cli/parse-opts ["--foo=bar"]) ;;=> {:foo "bar"}
Flags may be combined into a single short option:
(cli/parse-opts ["-abc"]) ;;=> {:a true :b true :c true}
Long options that start with --no- are parsed as negative flags:
(cli/parse-opts ["--no-colors"]) ;;=> {:colors false}
This works for any option. For a boolean option where the negation is meaningful,
set :negatable true in its spec to advertise it in help as --[no-]colors.
Babashka CLI also accepts a :-prefixed form, :opt val, to
match the Clojure CLI -X invocation style. The two forms cannot be mixed in a single
invocation. Use --/- or :, not both. If you prefer to only allow only --/- style options, specify :no-keyword-opts true:
(cli/parse-args [":foo" "bar"]) ;; => {:opts {:foo "bar"}} (cli/parse-args [":foo" "bar"] {:no-keyword-opts true}) ;; => {:args [":foo" "bar"], :opts {}} (cli/parse-args ["--foo" "bar" ":no" "mixing"]) ;; => {:args [":no" "mixing"], :opts {:foo "bar"}}
Notice how unrecognized options are considered Arguments.
A spec (short for options specification, not clojure.spec) is a map keyed by option
name; each value configures one option.
Alongside the parsing keys (:coerce, :alias, :validate, ...), it carries
:desc, :ref, and :default-desc used when printing options (see Printing options). For
example:
(def spec {:from {:ref "<format>" :desc "The input format. <format> can be edn, json or transit." :coerce :keyword :alias :i :default-desc "edn" :default :edn} :to {:ref "<format>" :desc "The output format. <format> can be edn, json or transit." :coerce :keyword :alias :o :default-desc "json" :default :json} :pretty {:desc "Pretty-print output." :alias :p} :paths {:desc "Paths of files to transform." :coerce [] :default ["src" "test"] :default-desc "src test"}})
You can pass the spec to parse-opts under the :spec key: (parse-opts args {:spec spec}), or when using
dispatch, in the entry for each command.
An explanation of each key:
:ref: a name that describes the option value, which is typically used as a reference in the description (:desc):desc: a description of the option.:coerce: coerce a string value to a type. Built-in keywords: :boolean
(:bool), :int (:long), :double, :number, :symbol, :keyword,
:string, :edn, :auto. A collection collects repeated values: []
(vector), #{} (set) or () (list); put a coercion keyword inside the collection to
coerce each element (e.g., [:keyword], #{:int}). A function is also
accepted: it is called with the option value as a string and returns the coerced value.:alias: an alternative short name; a synonym for the option name.:default: default value.:default-desc: a string representation of the default value.:require: true make this opt required.:validate: a function used to validate the value of this option (as described
in the Validate section).:collect: collect repeated values into a collection ([] vector, #{} set
or () list), or a function (fn [coll arg-value] ...) for custom collection:negatable: true shows a boolean option as --[no-]name in help (the --no-name form parses regardless)For those rare cases when you need it, you can use a :collect function for custom collection.
Here's an example of parsing out , separated multi-arg-values:
(cli/parse-opts ["--foo" "a,b" "--foo=c,d,e" "--foo" "f"] {:spec {:foo {:collect (fn [coll arg-value] (into (or coll []) (str/split arg-value #",")))}}}) ;; => {:foo ["a" "b" "c" "d" "e" "f"]}
Babashka CLI auto-coerces values that have no explicit coercion
with auto-coerce:
It automatically tries to convert booleans, numbers, and keywords.
(cli/parse-opts ["--num" "1339" "--kw" ":foo" "--bool" "false" "--str" "bar"]) ;; => {:num 1339, :kw :foo, :bool false, :str "bar"} ;; the actual types...: (->> (cli/parse-opts ["--num" "1339" "--kw" ":foo" "--bool" "false" "--str" "bar"]) (reduce-kv (fn [m k v] (assoc m k [v (type v)])) {})) ;; => {:num [1339 java.lang.Long], ;; :kw [:foo clojure.lang.Keyword], ;; :bool [false java.lang.Boolean], ;; :str ["bar" java.lang.String]}
:edn coercion behaves like Clojure CLI -X coercion:
(cli/parse-opts ["--foo" ":some-keyword"] {:spec {:foo {:coerce :edn}}}) ;; => {:foo :some-keyword} (cli/parse-opts ["--foo" "some-symbol"] {:spec {:foo {:coerce :edn}}}) ;; => {:foo some-symbol} (cli/parse-opts ["--foo" "\"some-string\""] {:spec {:foo {:coerce :edn}}}) ;; => {:foo "some-string"}
An :alias specifies a synonym short option name for the option name.
Babashka CLI distinguishes aliases with characters in common, so a way to implement the common -v/-vv Unix pattern is:
(def spec {:verbose {:alias :v :desc "Enable verbose output."} :very-verbose {:alias :vv :desc "Enable very verbose output."}})
You get:
(cli/parse-opts ["-v"] {:spec spec}) ;;=> {:verbose true} (cli/parse-opts ["-vv"] {:spec spec}) ;;=> {:very-verbose true}
Another way would be to collect the flags in a vector with :coerce (and base verbosity on the size of that vector):
(def spec {:verbose {:alias :v :desc "Enable verbose output." :coerce []}}) user=> (cli/parse-opts ["-vvv"] {:spec spec}) {:verbose [true true true]}
To parse positional arguments, you can use parse-args and/or the :args->opts
option. E.g., to parse arguments for the git push command:
(cli/parse-args ["--force" "ssh://foo"] {:spec {:force {:coerce :boolean}}}) ;;=> {:args ["ssh://foo"], :opts {:force true}} (cli/parse-args ["ssh://foo" "--force"] {:spec {:force {:coerce :boolean}}}) ;;=> {:args ["ssh://foo"], :opts {:force true}}
Note that babashka CLI can only disambiguate correctly between values for
options and trailing arguments with enough :coerce information
available. Without the :coerce :boolean info, we get:
(cli/parse-args ["--force" "ssh://foo"]) {:opts {:force "ssh://foo"}}
In case of ambiguity -- may also be used to communicate the boundary between
options and arguments:
(cli/parse-args ["--paths" "src" "test" "--" "ssh://foo"] {:spec {:paths {:coerce []}}}) {:args ["ssh://foo"], :opts {:paths ["src" "test"]}}
To fold positional arguments into the parsed options, you can use :args->opts:
(def cli-opts {:spec {:force {:coerce :boolean}} :args->opts [:url]}) (cli/parse-opts ["--force" "ssh://foo"] cli-opts) ;;=> {:force true, :url "ssh://foo"}
(cli/parse-opts ["ssh://foo" "--force"] cli-opts) ;;=> {:url "ssh://foo", :force true}
If you want to fold a variable number of arguments, you can coerce them into a vector
and specify the variable number of arguments with repeat:
(def cli-opts {:spec {:bar {:coerce []}} :args->opts (cons :foo (repeat :bar))}) (cli/parse-opts ["arg1" "arg2" "arg3" "arg4"] cli-opts) ;;=> {:foo "arg1", :bar ["arg2" "arg3" "arg4"]}
Options may be interspersed with the positional arguments:
(def cli-opts {:spec {:foo {:coerce :keyword} :bar {:coerce []} :force {:coerce :boolean}} :args->opts (cons :foo (repeat :bar))}) (cli/parse-opts ["arg1" "arg2" "--force" "arg3"] cli-opts) ;; => {:foo :arg1, :bar ["arg2" "arg3"], :force true}
This also holds for a command leaf in dispatch: a command with
variadic :args->opts parses options before, among, or after its positional
arguments. Without :args->opts, dispatch stops at the first positional argument
(to route commands), so trailing options would not be parsed.
The :positional marker tells Babashka CLI to explicitly treat an option as an argument so it can never be passed as an option.
(def cli-opts {:args->opts [:file] :spec {:file {:positional true}}}) (cli/parse-args ["a.txt"] cli-opts) ;;=> {:opts {:file "a.txt"}}
In case of an error, the output suggests it to be used as an argument:
(cli/parse-args [] {:args->opts [:file] :spec {:file {:positional true :require true}}}) ;; Required argument: <file> (cli/parse-args ["x"] {:args->opts [:n] :spec {:n {:positional true :coerce :long}}}) ;; Invalid value for argument <n>: cannot transform input "x" to long
Passing it as an option throws an error about not being able to use it as a command line option:
(cli/parse-args ["--file" "a.txt"] {:args->opts [:file] :spec {:file {:positional true}}}) ;; Not an option: --file (positional argument <file>)
In help, positional arguments are listed in an Arguments: section,
labeled by :ref (or key name) and described by :desc. A key without
:require is optional and shown in brackets. A :ref without brackets of its
own is wrapped in <>.
Set :restrict-args to error on positional arguments that :args->opts does
not consume. A variadic :args->opts consumes the remaining arguments, so none
are left to reject:
(cli/parse-args ["a" "b"] {:args->opts [:x] :restrict-args true}) ;; Unexpected argument: b
Babashka CLI handles commands with dispatch.
Say we want a CLI with a copy command, a delete command, and an undocumented debug command:
$ example copy <file> --dry-run
$ example delete <file> --recursive --depth 3
$ example debug
Commands can be specified in two ways: as a tree or a table.
The difference is more apparent for Multi-word Commands.
We'll use the tree structure for this example, save it to try_cmds.clj.
(ns try-cmds (:require [babashka.cli :as cli])) (defn copy [{:keys [opts]}] (prn :copy opts)) (defn delete [{:keys [opts]}] (prn :delete opts)) (def tree {:cmd {"copy" {:fn copy :doc "Copy a file\nMore details here" :args->opts [:file] :spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}} "delete" {:fn delete :doc "Delete a file" :args->opts [:file] :spec {:recursive {:coerce :boolean :desc "Recurse"} :depth {:coerce :long :desc "Max depth"}}} "debug" {:fn prn :doc "Dump internal state"}} ;; specify which commands to show and in what order (we exclude hidden debug command) :cmd-order ["copy" "delete"]}) (defn -main [& args] (cli/dispatch tree args {:prog "try-cmds" :help true}))
The same command structure expressed as a table is:
(def table [{:cmds ["copy"] :fn copy :doc "Copy a file\nMore details here" :args->opts [:file] :spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}} {:cmds ["delete"] :fn delete :doc "Delete a file" :args->opts [:file] :spec {:recursive {:coerce :boolean :desc "Recurse"} :depth {:coerce :long :desc "Max depth"}}} ;; hide debug command from usage help and completions with :no-doc {:cmds ["debug"] :fn prn :doc "Dump internal state" :no-doc true}]) (defn -main [& args] (cli/dispatch table args {:prog "try-cmds" :help true}))
The order of the entries in the table does not matter when matching commands, but it is used for --help.
Regardless of tree or table format, each command entry accepts any parse-args option (:spec,
:args->opts, :alias, :restrict, ...).
Note
If you want to try try_cmds.clj from your terminal:
For babashka, create bb.edn in the same dir:
{:paths ["."]}Then run with bb -m try-cmds ... (Our examples below use babashka).
For Clojure, create a deps.edn in the same dir:
{:paths ["."] :deps {org.babashka/cli {:mvn/version "<latest-version>"}}}Then run with clojure -M -m try-cmds ...
dispatch matches the given command line args against specified commands and calls the matching entry's :fn with the parsed result.
:help true wires up --help/-h and prints terse errors instead of throwing exceptions (see Help):
$ bb -m try-cmds --help
Usage: try-cmds [options] <command>
Commands:
copy Copy a file
delete Delete a file
Options:
-h, --help Show this help
Run "try-cmds <command> --help" for more information on a command.
The Commands: descriptions come from each command entry's :doc key.
The first line of :doc is used as a summary for the command.
The debug command is absent in the help because it is absent from :cmd-order (it was suppressed in table format via :no-doc true). This also hides debug from completions.
Tip
Like :cmd-order, options can be hidden with :order.
And :no-doc true can also be used on an option to hide it.
Hiding works well for deprecated or internal commands and options.
The full text of :doc is shown as the description on the command's --help output, between the usage line and Options::
$ bb -m try-cmds copy --help
Usage: try-cmds copy [options] <file>
Copy a file
More details here
Options:
--dry-run Do a dry run
-h, --help Show this help
:doc (and :epilog) also accept a vector of strings, joined with
newlines. Longer texts read better that way, especially in edn files, where
str/join is not available:
:doc ["Copy a file" "" "More details here"]
Running bb -m try-cmds copy the-file --dry-run calls copy, which prints:
:copy {:file "the-file", :dry-run true}The copy command entry :fn is called with a map of the parsed result:
:opts: the parsed options ({:file "the-file" :dry-run true}; :file comes
from :args->opts):dispatch: the matched command path ["copy"] from the given dispatch command structure.:args: any leftover positional args (nil here)An unknown or missing command prints a terse message and exits the process with a status of 1:
$ bb -m try-cmds bogus
Unknown command: bogus
Commands:
copy Copy a file
delete Delete a file
Run "try-cmds --help" for more information.
Sometimes a command can be made up of multiple words.
Think of git, for example. We have git remote, git remote add ..., git remote delete ..., etc.
The command hierarchy is:
remote (level 1) is a parent of both:
remote add (level 2)remote delete (level 2)A dispatch tree might be expressed as:
{:cmd {"remote"
{:fn remotes-list :doc "show list of remotes"
:cmd {"add" {:fn remote-add :doc "add a new remote"}
"delete" {:fn remote-delete :doc "delete a remote"}}
:cmd-order ["add" "delete"]}}}The same commands, expressed as a dispatch table:
[{:cmds ["remote"] :fn remotes-list :doc "show list of remotes"}
{:cmds ["remote" "add"] :fn remote-add :doc "add a new remote"}
{:cmds ["remote" "delete"] :fn remote-delete :doc "delete a remote"}]Multi-word commands are matched from the command line in the specified order, and the longest matching entry's :fn is called.
So git remote add would result in a call to the remote-add :fn and not the remotes-list :fn.
A command line can have options before and between commands and command hierarchy levels.
Root-level options are specified in the root spec. A contrived example to illustrate:
(def root-spec {:foo {:coerce #(str "global-" %)}}) (def sub1-spec {:bar {:coerce #(str "sub1-" %)}}) (def sub2-spec {:baz {:coerce #(str "sub2-" %)}}) (def tree {:spec root-spec :cmd {"sub1" {:fn identity :spec sub1-spec :cmd {"sub2" {:fn identity :spec sub2-spec}}}}}) (cli/dispatch tree ["--foo" "a" "sub1" "--bar" "b" "sub2" "--baz" "c" "arg"]) ;; => {:dispatch ["sub1" "sub2"], ;; :opts {:foo "global-a", :bar "sub1-b", :baz "sub2-c"}, ;; :args ["arg"]}
(def table [{:cmds [] :spec root-spec} ;; root spec specified with `:cmds []` {:cmds ["sub1"] :fn identity :spec sub1-spec} {:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}])
Specs are not merged across command hierarchy levels. An option is parsed with the spec of the current matching command (while parsing the command line from left to right):
--foo a appears before any matching command, so is coerced with root-spec--bar b appears after a matching sub1 but before sub2, so is coerced with sub1's sub1-spec--baz c appears after matching sub1 and sub2, so is coerced with sub2s sub2-specLet's compare with a different ordering on the command line:
(cli/dispatch tree ["--foo" "a" "sub1" "sub2" "--bar" "b" "--baz" "c" "arg"]) ;; => {:dispatch ["sub1" "sub2"], ;; :opts {:foo "global-a", :bar "b", :baz "sub2-c"}, ;; :args ["arg"]}
Notice that --bar is now after sub1 and sub2 and gets default string coercion treatment.
This is because it was processed with sub2-spec, which has no specific coercion for :bar.
Let's explore how :restrict works with command hierarchies.
(def tree {:cmd {"group" {:spec {:registry {}} :cmd {"sub" {:fn identity :spec {:format {}}}}}}})
(def table [{:cmds ["group"] :spec {:registry {}}} {:cmds ["group" "sub"] :fn identity :spec {:format {}}}])
Because :registry belongs to the group command, it is expected only to be used with the group command:
(cli/dispatch tree ["group" "--registry" "X" "sub"] {:restrict true}) ;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
and not the group sub command:
(cli/dispatch tree ["group" "sub" "--registry" "X"] {:restrict true}) ;; throws: Unknown option: --registry
Mark an option with :inherit true to also accept it at any command hierarchy descendant level.
The option is coerced and restrict-checked wherever it appears:
(def tree {:cmd {"group" {:spec {:registry {:inherit true}} ;; <-- :cmd {"sub" {:fn identity :spec {:format {}}}}}}}) (cli/dispatch tree ["group" "sub" "--registry" "X"] {:restrict true}) ;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
(def table [{:cmds ["group"] :spec {:registry {:inherit true}}} ;; <-- {:cmds ["group" "sub"] :fn identity :spec {:format {}}}])
A descendant command may redefine an option in its own spec, in which case the descendant's spec wins.
Instead of marking individual options, you can pass an :inherit option to dispatch.
Specify true to inherit all options, or a set of keys to inherit only those options:
(def tree {:cmd {"group" {:spec {:registry {}} :cmd {"sub" {:fn identity :spec {:format {}}}}}}}) (cli/dispatch tree ["group" "sub" "--registry" "X"] {:inherit true}) ;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil} (cli/dispatch tree ["group" "sub" "--registry" "X"] {:inherit #{:registry}}) ;; => {:dispatch ["group" "sub"], :opts {:registry "X"}, :args nil}
You can use :args->opts, but command matching is always prioritized first:
(def tree {:cmd {"sub1" {:fn identity :spec sub1-spec :args->opts [:some-opt] :cmd {"sub2" {:fn identity :spec sub2-spec}}}}}) (cli/dispatch tree ["sub1" "dude"]) ;; => {:dispatch ["sub1"], :opts {:some-opt "dude"}, :args nil} (cli/dispatch tree ["sub1" "sub2"]) ;; => {:dispatch ["sub1" "sub2"], :opts {}, :args nil}
(def table [{:cmds ["sub1"] :fn identity :spec sub1-spec :args->opts [:some-opt]} {:cmds ["sub1" "sub2"] :fn identity :spec sub2-spec}])
See neil for a real-world CLI using multi-word commands.
Commands can be specified in a tree or table format. Both formats are supported; use the format that best suits you.
The difference between formats becomes apparent when multi-word commands are used. For example, let's say we have a CLI with commands:
copy (hierarchy level 1)cache (hierarchy level 1)cache clean (hierarchy level 2)The table format represents this structure flatly:
(def table [{:cmds [] :spec {:verbose {:coerce :boolean :inherit true :desc "Verbose output"}}} ;; top-level options {:cmds ["copy"] :fn copy :doc "Copy a file" :args->opts [:file] :spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}} {:cmds ["cache"] :doc "Manage the cache"} {:cmds ["cache" "clean"] :fn clean :doc "Clean the cache"}]) (cli/dispatch table args {:prog "example" :help true})
The tree format, as you would gu
| |
| |
| |
|
| |||||||
| |||||||
|