| name | clojure-babashka-cli |
| description | Turn Clojure functions into CLIs with babashka.cli. Use when working with command-line argument parsing, building CLIs, subcommand dispatching, option validation, or creating tools with babashka/clojure. |
babashka.cli
Turn Clojure functions into CLIs with minimal effort. Supports both :foo value and --foo value style arguments, automatic type coercion, validation, subcommands, and help generation.
Setup
deps.edn:
org.babashka/cli {:mvn/version "0.8.60"}
Leiningen:
[org.babashka/cli "0.8.60"]
Built into babashka - no dependency needed in bb.edn.
See https://clojars.org/org.babashka/cli for the latest version.
Quick Start
#!/usr/bin/env bb
(require '[babashka.cli :as cli])
(defn greet [{:keys [name verbose]}]
(when verbose (println "Greeting..."))
(println "Hello" name))
;; Simple parsing
(cli/parse-opts ["--name" "Alice" "--verbose"] {:coerce {:verbose :boolean}})
;;=> {:name "Alice", :verbose true}
;; Call function directly with parsed args
(greet (cli/parse-opts *command-line-args* {:coerce {:verbose :boolean}}))
Core Patterns
Parse Options
parse-opts - returns a map, leaves extra args in metadata:
(cli/parse-opts ["--port" "8080" "--host" "0.0.0.0"]
{:coerce {:port :long}})
;;=> {:port 8080, :host "0.0.0.0"}
parse-args - separates options from positional arguments:
(cli/parse-args ["--force" "file.txt"]
{:coerce {:force :boolean}})
;;=> {:opts {:force true}, :args ["file.txt"]}
Type Coercion
;; Keyword coercion types
{:coerce {:port :long ; parse-long
:ratio :double ; parse-double
:flag :boolean ; true/false
:name :string ; stays string
:kw :keyword ; keyword
:sym :symbol}} ; symbol
;; Auto-coercion (enabled by default since v0.3.35)
(cli/parse-opts ["--port" "8080"]) ;;=> {:port 8080}
(cli/parse-opts ["--flag"]) ;;=> {:flag true}
(cli/parse-opts ["--key" ":value"]) ;;=> {:key :value}
;; Collection coercion
{:coerce {:paths []}} ; collect into vector
(cli/parse-opts ["--paths" "src" "--paths" "test"] {:coerce {:paths []}})
;;=> {:paths ["src" "test"]}
{:coerce {:tags [:keyword]}} ; vector of keywords
(cli/parse-opts ["--tags" "foo" "bar"] {:coerce {:tags [:keyword]}})
;;=> {:tags [:foo :bar]}
Aliases (Short Options)
(cli/parse-opts ["-p" "8080" "-v"]
{:alias {:p :port, :v :verbose}
:coerce {:port :long, :verbose :boolean}})
;;=> {:port 8080, :verbose true}
;; Combined short flags (since v0.7.51)
(cli/parse-opts ["-abc"])
;;=> {:a true, :b true, :c true}
Positional Arguments
Use :args->opts to map positional arguments to option names:
;; Fixed number of args
(cli/parse-opts ["alice" "30"]
{:args->opts [:name :age]
:coerce {:age :long}})
;;=> {:name "alice", :age 30}
;; Variable args - first goes to :user, rest to :files
(cli/parse-opts ["alice" "a.txt" "b.txt"]
{:args->opts (cons :user (repeat :files))
:coerce {:files []}})
;;=> {:user "alice", :files ["a.txt" "b.txt"]}
Validation and Requirements
(def spec
{:port {:coerce :long
:validate pos?
:require true
:desc "Server port"}
:dir {:validate #(fs/directory? %)
:desc "Working directory"}})
;; Validate with custom message
{:host {:validate {:pred #(re-matches #"[a-z0-9.-]+" %)
:ex-msg (fn [m] (str "Invalid host: " (:value m)))}}}
;; Require specific options
(cli/parse-opts [] {:require [:port]})
;;=> Throws: Required option: :port
;; Restrict to known options only
(cli/parse-opts ["--port" "8080" "--unknown" "x"]
{:spec {:port {}} :restrict true})
;;=> Throws: Unknown option: :unknown
Default Values
(cli/parse-opts ["--name" "Alice"]
{:exec-args {:port 8080, :host "localhost"}})
;;=> {:name "Alice", :port 8080, :host "localhost"}
;; Command-line args override defaults
(cli/parse-opts ["--port" "9000"]
{:exec-args {:port 8080}})
;;=> {:port 9000}
Subcommands
Use dispatch for subcommand routing:
(defn copy-cmd [{:keys [opts]}]
(println "Copying" (:file opts)))
(defn delete-cmd [{:keys [opts]}]
(println "Deleting" (:file opts) "depth:" (:depth opts)))
(def table
[{:cmds ["copy"] :fn copy-cmd :args->opts [:file]}
{:cmds ["delete"] :fn delete-cmd :args->opts [:file]
:coerce {:depth :long}}
{:cmds [] :fn (fn [_] (println "Unknown command"))}])
(cli/dispatch table ["copy" "foo.txt"])
;;=> Copying foo.txt
(cli/dispatch table ["delete" "bar.txt" "--depth" "3"])
;;=> Deleting bar.txt depth: 3
Shared options across subcommands (since v0.8.54):
(def table
[{:cmds [] :spec {:verbose {:coerce :boolean}}} ; global option
{:cmds ["build"] :fn build-cmd :spec {:target {:coerce :keyword}}}
{:cmds ["test"] :fn test-cmd}])
(cli/dispatch table ["--verbose" "build" "--target" "prod"])
;;=> {:dispatch ["build"], :opts {:verbose true, :target :prod}}
Help Generation
Use format-opts to generate help text:
(def spec
{:port {:desc "Server port"
:ref "<port>"
:default 8080
:coerce :long
:alias :p}
:host {:desc "Server host"
:ref "<host>"
:default-desc "localhost"
:alias :h}
:verbose {:desc "Enable verbose logging"
:alias :v}})
(println (cli/format-opts {:spec spec :order [:port :host :verbose]}))
;; Prints:
;; -p, --port <port> 8080 Server port
;; -h, --host <host> localhost Server host
;; -v, --verbose Enable verbose logging
Error Handling
Custom error handler:
(cli/parse-opts []
{:spec {:port {:require true :desc "Server port"}}
:error-fn
(fn [{:keys [type cause msg option]}]
(when (= :org.babashka/cli type)
(case cause
:require (println "Missing required:" option)
:validate (println "Invalid value:" msg)
:coerce (println "Cannot parse:" msg)
:restrict (println "Unknown option:" option))
(System/exit 1)))})
Common CLI Patterns
Complete CLI with Help
(def cli-spec
{:spec
{:port {:coerce :long
:desc "Server port"
:default 8080
:alias :p}
:verbose {:coerce :boolean
:desc "Verbose output"
:alias :v}
:help {:coerce :boolean
:desc "Show help"
:alias :h}}})
(defn show-help []
(println "Usage: server [options]")
(println (cli/format-opts cli-spec)))
(defn -main [& args]
(let [opts (cli/parse-opts args cli-spec)]
(if (:help opts)
(show-help)
(start-server opts))))
Building Tasks for Babashka
In bb.edn:
{:tasks
{build {:doc "Build the project"
:task (let [opts (cli/parse-opts *command-line-args*
{:coerce {:clean :boolean}})]
(when (:clean opts) (clean))
(compile-project opts))}}}
Usage: bb build --clean
Clojure CLI Integration
Call any function as a CLI without wrapper code. In deps.edn:
{:aliases
{:exec {:extra-deps {org.babashka/cli {:mvn/version "0.8.60"}}
:main-opts ["-m" "babashka.cli.exec"]}}}
Usage:
;; Call any function with map arg
clojure -M:exec my-ns/my-fn --port 8080 --verbose
;; Add metadata to function for coercion
(defn my-fn
{:org.babashka/cli {:coerce {:port :long}}}
[{:keys [port verbose]}]
...)
Key Gotchas
Options are open by default - extra options don't cause errors. Use
:restrict trueto enforce known options only.Without
:coerceinfo, ambiguous args default to strings:
(cli/parse-args ["--port" "8080"]) ;;=> {:port "8080"} (string!)
- Boolean flags need no value - just their presence:
(cli/parse-opts ["--verbose"]) ;;=> {:verbose true}
- Negative flags work automatically (since v0.7.51):
(cli/parse-opts ["--no-colors"]) ;;=> {:colors false}
- Use
--to separate options from arguments when ambiguous:
(cli/parse-args ["--paths" "src" "test" "--" "file.txt"]
{:coerce {:paths []}})
;;=> {:opts {:paths ["src" "test"]}, :args ["file.txt"]}
- The library is still evolving - check CHANGELOG for breaking changes before upgrading.
References
- Full API Reference - Complete function signatures and documentation
- GitHub README - Comprehensive examples and guides
- Babashka Book CLI Chapter - Integration with babashka tasks