Cli
Turn Clojure functions into CLIs!
Install / Use
/learn @babashka/CliREADME
babashka.cli
Turn Clojure functions into CLIs! This library can be used from:
- babashka - included as a built-in library
- Clojure on the JVM - we support Clojure 1.10.3 and above on Java 11 and above
- ClojureScript - we test against the current release
API
Status
This library is still in design phase and may still undergo breaking changes. Check breaking changes before upgrading!
Installation
Add to your deps.edn or bb.edn :deps entry:
org.babashka/cli {:mvn/version "<latest-version>"}
Intro
Command line arguments in clojure and babashka CLIs are often in the form:
$ cli command :opt1 v1 :opt2 v2
or the more Unixy:
$ cli command --long-opt1 v1 -o v2
The main ideas:
- Put as little effort as possible into turning a clojure function into a CLI,
similar to
-Xstyle invocations. For lazy people like me! If you are not familiar withclj -X, read the docs here. - But with a better UX by not having to use quotes on the command line as a
result of having to pass EDN directly:
:dir fooinstead of:dir '"foo"'or who knows how to write the latter incmd.exeor Powershell. - By default, employ an open world assumption: passing extra arguments does not break and arguments can be re-used in multiple contexts.
- But also support incremental validations as a form of polishing a cli for production use.
Both : and -- are supported as the initial characters of a named option, but
cannot be mixed. See options for more details.
See clojure CLI for how to turn your exec functions into CLIs.
Projects using babashka CLI
TOC
- Simple example
- Options
- Arguments
- Adding Production Polish
- Subcommands
- Babashka tasks
- Clojure CLI
- Leiningen
Simple example
Babashka cli works in Clojure, ClojureScript and babashka. Here is an example babashka script to get you started!
#!/usr/bin/env bb
(require '[babashka.cli :as cli]
'[babashka.fs :as fs])
(defn dir-exists?
[path]
(fs/directory? path))
(defn show-help
[spec]
(cli/format-opts (merge spec {:order (vec (keys (:spec spec)))})))
(def cli-spec
{:spec
{:num {:coerce :long
:desc "Number of some items"
:alias :n ; adds -n alias for --num
:validate pos? ; tests if supplied --num >0
:require true} ; --num,-n is required
:dir {:desc "Directory name to do stuff"
:alias :d
:validate dir-exists?} ; tests if --dir exists
:flag {:coerce :boolean ; defines a boolean flag
:desc "I am just a flag"}}
:error-fn ; a function to handle errors
(fn [{:keys [spec type cause msg option] :as data}]
(when (= :org.babashka/cli type)
(case cause
:require
(println
(format "Missing required argument: %s\n" option))
:validate
(println
(format "%s does not exist!\n" msg)))))})
(defn -main
[args]
(let [opts (cli/parse-opts args cli-spec)]
(if (or (:help opts) (:h opts))
(println (show-help cli-spec))
(println "Here are your cli args!:" opts))))
(-main *command-line-args*)
And this is how you run it:
$ bb try-me.clj --num 1 --dir my_dir --flag
Here are your cli args!: {:num 1, :dir my_dir, :flag true}
$ bb try-me.clj --help
Missing required argument: :num
-n, --num Number of some items
-d, --dir Directory name to do stuff
--flag I am just a flag
Using the spec format is optional and you can implement you own parsing logic just with parse-opts/parse-args.
However, many would find the above example familiar.
Options
For parsing options, use either parse-opts or parse-args.
Examples:
Parse {:port 1339} from command line arguments:
(require '[babashka.cli :as cli])
(cli/parse-opts ["--port" "1339"] {:coerce {:port :long}})
;;=> {:port 1339}
Use an alias (short option):
(cli/parse-opts ["-p" "1339"] {:alias {:p :port} :coerce {:port :long}})
;; {:port 1339}
Coerce values into a collection:
(cli/parse-opts ["--paths" "src" "--paths" "test"] {:coerce {:paths []}})
;;=> {:paths ["src" "test"]}
(cli/parse-opts ["--paths" "src" "test"] {:coerce {:paths []}})
;;=> {:paths ["src" "test"]}
Transforming to a collection of a certain type:
(cli/parse-opts ["--foo" "bar" "--foo" "baz"] {:coerce {:foo [:keyword]}})
;; => {:foo [:bar :baz]}
Booleans need no explicit true value and :coerce option:
(cli/parse-opts ["--verbose"])
;;=> {:verbose true}
(cli/parse-opts ["-v" "-v" "-v"] {:alias {:v :verbose}
:coerce {:verbose []}})
;;=> {: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 (since 0.7.51):
(cli/parse-opts ["-abc"])
;;=> {:a true :b true :c true}
Arguments that start with --no- arg parsed as negative flags (since 0.7.51):
(cli/parse-opts ["--no-colors"])
;;=> {:colors false}
Custom collection handling
Usually the above will suffice, but for custom transformation to a collection, you can use :collect.
Here's an example of parsing out , separated multi-arg-values:
(cli/parse-opts ["--foo" "a,b" "--foo=c,d,e" "--foo" "f"]
{:collect {:foo (fn [coll arg-value]
(into (or coll [])
(str/split arg-value #",")))}})
;; => {:foo ["a" "b" "c" "d" "e" "f"]}
Auto-coercion
Since v0.3.35 babashka CLI auto-coerces values that have no explicit coercion
with
auto-coerce:
it automatically tries to convert booleans, numbers and keywords.
Arguments
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"] {:coerce {:force :boolean}})
;;=> {:args ["ssh://foo"], :opts {:force true}}
(cli/parse-args ["ssh://foo" "--force"] {:coerce {:force :boolean}})
;;=> {:args ["ssh://foo"], :opts {:force true}}
Note that this library can only disambiguate correctly between values for
options and trailing arguments with enough :coerce information
available. Without the :force :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"] {:coerce {:paths []}})
{:args ["ssh://foo"], :opts {:paths ["src" "test"]}}
:args->opts
To fold positional arguments into the parsed options, you can use :args->opts:
(def cli-opts {:coerce {:force :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 amount of arguments, you can coerce into a vector
and specify the variable number of arguments with repeat:
(def cli-opts {:coerce {:bar []} :args->opts (cons :foo (repeat :bar))})
(cli/parse-opts ["arg1" "arg2" "arg3" "arg4"] cli-opts)
;;=> {:foo "arg1", :bar ["arg2" "arg3" "arg4"]}
Adding Production Polish
Babashka cli lets you get up and running quickly. As you move toward production quality, it’s helpful to let users know when their inputs are invalid. Strict validation can be introduced with :restrict, :require, and :validate.
As you add polish, you'll likely make use of a :spec, a custom :error_fn, and maybe subcommand dispatching.
Restrict
Use the :restrict option to restrict options to only those explicitly mentioned in configuration:
(cli/parse-args ["--foo"] {:restrict [:bar]})
;;=>
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:357).
Unknown option: :foo
Require
Use the :require option to throw an error when an option is not present:
(cli/parse-args ["--foo"] {:require [:bar]})
;;=>
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:363).
Required option: :bar
Validate
(cli/parse-args ["--foo" "0"] {:validate {:foo pos?}})
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:378).
Invalid value for option :foo: 0
To gain more control over the error message, use :pred and :ex-msg:
(cli/parse-args ["--foo" "0"] {:validate {:foo {:pred pos? :ex-msg (fn [m] (str "Not a positiv
