clojure-babashka-cli
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
More from rcmerci/skills
logseq-schema
Logseq Datascript schema, built-in properties/classes, and :db/ident discovery for composing or reviewing Datascript queries about blocks/pages/tags/properties/classes. Use whenever editing or reviewing Datascript pull selectors or queries, or any code that adds/removes attributes in pull patterns, or touches property namespaces/identifiers, or requires reasoning about property value shapes/ref/cardinality in Logseq.
40logseq-cli
Operate the current Logseq command-line interface to inspect or modify graphs, pages, blocks, tags, and properties; run Datascript queries; show page/block trees; manage graphs; and manage db-worker-node servers. Use when a request involves running `logseq` commands or interpreting CLI output.
31logseq-electron-debug
Run Logseq Electron dev build with a remote debugging port and attach Chrome DevTools (CDP) reliably.
6clojure-debug
Debugging workflow for Clojure/ClojureScript code. Use at the first sign of unexpected behavior or test failure in Clojure/CLJS, including any failing test, unexpected output, nils where data is expected, mismatched selectors, or unclear data flow—before making further implementation changes.
4clojure-malli
|
3clojure-paren-repair
Repair unbalanced parentheses, brackets, and braces in Clojure, ClojureScript, and EDN files. Use when you encounter delimiter mismatch syntax errors after editing .clj, .cljs, .cljc, or .edn files, or on clojure syntax errors.
3