SkillAgentSearch skills...

Cli

Turn Clojure functions into CLIs!

Install / Use

/learn @babashka/Cli
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

babashka.cli

Clojars Project bb built-in

Turn Clojure functions into CLIs! This library can be used from:

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 -X style invocations. For lazy people like me! If you are not familiar with clj -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 foo instead of :dir '"foo"' or who knows how to write the latter in cmd.exe or 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

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
View on GitHub
GitHub Stars270
CategoryDevelopment
Updated16h ago
Forks19

Languages

Clojure

Security Score

100/100

Audited on Mar 28, 2026

No findings