clojure-babashka-cli
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.
When & Why to Use This Skill
This Claude skill facilitates the transformation of Clojure functions into robust Command Line Interfaces (CLIs) using the babashka.cli library. It streamlines the development process by automating complex tasks such as argument parsing, type coercion, subcommand dispatching, and help text generation, making it an essential utility for developers building automation scripts and developer tools within the Clojure and Babashka ecosystems.
Use Cases
- Converting existing Clojure functions into standalone CLI tools with minimal boilerplate code.
- Building structured Babashka tasks for project automation, such as build scripts, database migrations, or deployment triggers.
- Developing complex command-line applications that require nested subcommands and automated 'help' documentation.
- Implementing robust input validation and type coercion for script arguments to prevent runtime errors in automation workflows.
- Integrating Clojure-based utilities into CI/CD pipelines by providing a clean, standardized interface for command-line execution.
| 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