Claude Code Plugins

Community-maintained marketplace

Feedback

Command-line argument parsing for turning Clojure functions into CLIs

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name babashka.cli
description Command-line argument parsing for turning Clojure functions into CLIs

babashka.cli

Command-line argument parsing library for transforming Clojure functions into CLIs with minimal effort.

Overview

babashka.cli converts command-line arguments into Clojure data structures, supporting both keyword-style (:opt value) and Unix-style (--opt value) arguments. Designed to minimize friction when creating CLIs from existing Clojure functions.

Key Features:

  • Automatic type coercion
  • Flexible argument syntax (:foo or --foo)
  • Subcommand dispatch
  • Validation and error handling
  • Boolean flags and negative flags
  • Collection handling for repeated options
  • Default values

Artifact: org.babashka/cli Latest Version: 0.8.60 License: MIT Repository: https://github.com/babashka/cli

Installation

Add to deps.edn:

{:deps {org.babashka/cli {:mvn/version "0.8.60"}}}

Or bb.edn for Babashka:

{:deps {org.babashka/cli {:mvn/version "0.8.60"}}}

Since babashka 0.9.160, babashka.cli is built-in.

Core Concepts

Parsing vs. Args Separation

  • parse-opts - Returns flat map of parsed options
  • parse-args - Separates into :opts, :cmds, :rest-args

Open World Assumption

Extra arguments don't cause errors by default. Use :restrict for strict validation.

Coercion Strategy

Values are coerced based on specifications, not inferred from values alone. This ensures predictable type handling.

API Reference

Parsing Functions

parse-opts

Parse command-line arguments into options map.

(require '[babashka.cli :as cli])

;; Basic parsing
(cli/parse-opts ["--port" "8080"])
;;=> {:port "8080"}

;; With coercion
(cli/parse-opts ["--port" "8080"] {:coerce {:port :long}})
;;=> {:port 8080}

;; With aliases
(cli/parse-opts ["-p" "8080"]
  {:alias {:p :port}
   :coerce {:port :long}})
;;=> {:port 8080}

Options:

  • :coerce - Type coercion map (:boolean, :int, :long, :double, :symbol, :keyword)
  • :alias - Short name to long name mappings
  • :spec - Structured option specifications
  • :restrict - Restrict to specified options only
  • :require - Required option keys
  • :validate - Validation predicates
  • :exec-args - Default values
  • :args->opts - Map positional args to option keys
  • :no-keyword-opts - Only accept --foo style (not :foo)
  • :error-fn - Custom error handler

parse-args

Parse arguments with separation of options, commands, and rest args.

(cli/parse-args ["--verbose" "deploy" "prod" "--force"]
  {:coerce {:verbose :boolean :force :boolean}})
;;=> {:cmds ["deploy" "prod"]
;;    :opts {:verbose true :force true}
;;    :rest-args []}

Returns map with:

  • :opts - Parsed options
  • :cmds - Subcommands (non-option arguments)
  • :rest-args - Arguments after --

parse-cmds

Extract subcommands from arguments.

(cli/parse-cmds ["deploy" "prod" "--force"])
;;=> {:cmds ["deploy" "prod"]
;;    :args ["--force"]}

;; Without keyword opts
(cli/parse-cmds ["deploy" ":env" "prod"]
  {:no-keyword-opts true})
;;=> {:cmds ["deploy" ":env" "prod"]
;;    :args []}

Coercion

Type Keywords

  • :boolean - True/false values
  • :int - Integer
  • :long - Long integer
  • :double - Floating point
  • :symbol - Clojure symbol
  • :keyword - Clojure keyword

Collection Coercion

Use empty vector to collect multiple values:

(cli/parse-opts ["--path" "src" "--path" "test"]
  {:coerce {:path []}})
;;=> {:path ["src" "test"]}

Typed collections:

(cli/parse-opts ["--port" "8080" "--port" "8081"]
  {:coerce {:port [:long]}})
;;=> {:port [8080 8081]}

auto-coerce

Automatic coercion for unspecified options (enabled by default):

(cli/parse-opts ["--enabled" "true" "--count" "42" "--mode" ":prod"])
;;=> {:enabled true :count 42 :mode :prod}

Converts:

  • "true"/"false" → boolean
  • Numeric strings → numbers via edn/read-string
  • Strings starting with : → keywords

Boolean Flags

;; Flag present = true
(cli/parse-opts ["--verbose"])
;;=> {:verbose true}

;; Combined short flags
(cli/parse-opts ["-vvv"])
;;=> {:v true}

;; Negative flags
(cli/parse-opts ["--no-colors"])
;;=> {:colors false}

;; Explicit values
(cli/parse-opts ["--force" "false"]
  {:coerce {:force :boolean}})
;;=> {:force false}

Positional Arguments

Basic args->opts

Map positional arguments to named options:

(cli/parse-opts ["deploy" "production"]
  {:args->opts [:action :env]})
;;=> {:action "deploy" :env "production"}

Variable Length Collections

Use repeat for collecting remaining args:

(cli/parse-opts ["build" "foo.clj" "bar.clj" "baz.clj"]
  {:args->opts (cons :cmd (repeat :files))
   :coerce {:files []}})
;;=> {:cmd "build" :files ["foo.clj" "bar.clj" "baz.clj"]}

Mixed Options and Arguments

(cli/parse-opts ["--verbose" "deploy" "prod" "--force"]
  {:coerce {:verbose :boolean :force :boolean}
   :args->opts [:action :env]})
;;=> {:verbose true :action "deploy" :env "prod" :force true}

Validation

Required Options

(cli/parse-args ["--name" "app"]
  {:require [:name :version]})
;; Throws: Required option: :version

Restricted Options

(cli/parse-args ["--verbose" "--debug"]
  {:restrict [:verbose]})
;; Throws: Unknown option: :debug

Custom Validators

(cli/parse-args ["--port" "0"]
  {:coerce {:port :long}
   :validate {:port pos?}})
;; Throws: Invalid value for option :port: 0

;; With custom message
(cli/parse-args ["--port" "-1"]
  {:coerce {:port :long}
   :validate {:port {:pred pos?
                      :ex-msg (fn [{:keys [option value]}]
                                (str option " must be positive, got: " value))}}})
;; Throws: :port must be positive, got: -1

Default Values

Provide defaults via :exec-args:

(cli/parse-args ["--port" "9000"]
  {:coerce {:port :long}
   :exec-args {:port 8080 :host "localhost"}})
;;=> {:opts {:port 9000 :host "localhost"}}

Error Handling

Custom error handler:

(defn error-handler [{:keys [type cause msg option]}]
  (when (= type :org.babashka/cli)
    (println "Error:" msg)
    (when option
      (println "Option:" option))
    (System/exit 1)))

(cli/parse-args ["--invalid"]
  {:restrict [:valid]
   :error-fn error-handler})

Error causes:

  • :restrict - Unknown option
  • :require - Missing required option
  • :validate - Validation failed
  • :coerce - Type coercion failed

Subcommand Dispatch

dispatch

Route execution based on subcommands:

(defn deploy [opts]
  (println "Deploying to" (:env opts)))

(defn rollback [opts]
  (println "Rolling back" (:version opts)))

(def table
  [{:cmds ["deploy"] :fn deploy :args->opts [:env]}
   {:cmds ["rollback"] :fn rollback :args->opts [:version]}
   {:cmds [] :fn (fn [_] (println "No command specified"))}])

(cli/dispatch table ["deploy" "production"])
;; Prints: Deploying to production

(cli/dispatch table ["rollback" "v1.2.3"])
;; Prints: Rolling back v1.2.3

Nested Subcommands

(def table
  [{:cmds ["db" "migrate"] :fn db-migrate}
   {:cmds ["db" "rollback"] :fn db-rollback}
   {:cmds ["db"] :fn (fn [_] (println "db requires subcommand"))}])

(cli/dispatch table ["db" "migrate" "--env" "prod"])

Dispatch Options

Pass options to parse-args:

(cli/dispatch table args
  {:coerce {:port :long}
   :exec-args {:host "localhost"}})

The :fn receives enhanced parse-args result:

  • :dispatch - Matched command path
  • :args - Remaining unparsed arguments
  • :opts - Parsed options
  • :cmds - Subcommands

Formatting & Help

format-opts

Generate help text from spec:

(def spec
  {:port {:desc "Port to listen on"
          :default 8080
          :coerce :long}
   :host {:desc "Host address"
          :default "localhost"
          :alias :h}
   :verbose {:desc "Enable verbose output"
             :alias :v}})

(println (cli/format-opts {:spec spec}))
;; Output:
;;   --port       Port to listen on (default: 8080)
;;   --host, -h   Host address (default: localhost)
;;   --verbose, -v Enable verbose output

With custom indent:

(cli/format-opts {:spec spec :indent 4})

format-table

Format tabular data:

(cli/format-table
  {:rows [["Name" "Type" "Default"]
          ["port" "long" "8080"]
          ["host" "string" "localhost"]]
   :indent 2})

spec->opts

Convert spec to parse options:

(def spec
  {:port {:ref "<port>"
          :desc "Server port"
          :coerce :long
          :default 8080}})

(cli/spec->opts spec)
;;=> {:coerce {:port :long}}

(cli/spec->opts spec {:exec-args true})
;;=> {:coerce {:port :long} :exec-args {:port 8080}}

Option Merging

merge-opts

Combine multiple option specifications:

(def base-opts
  {:coerce {:verbose :boolean}})

(def server-opts
  {:coerce {:port :long}
   :exec-args {:port 8080}})

(cli/merge-opts base-opts server-opts)
;;=> {:coerce {:verbose :boolean :port :long}
;;    :exec-args {:port 8080}}

Common Patterns

CLI Application Entry Point

#!/usr/bin/env bb

(ns my-app
  (:require [babashka.cli :as cli]))

(defn run [{:keys [port host verbose]}]
  (when verbose
    (println "Starting server on" host ":" port))
  ;; ... server logic
  )

(def spec
  {:port {:desc "Port to listen on"
          :coerce :long
          :default 8080}
   :host {:desc "Host address"
          :default "localhost"}
   :verbose {:desc "Enable verbose output"
             :alias :v
             :coerce :boolean}})

(defn -main [& args]
  (cli/parse-args args
    {:spec spec
     :exec-args (:default spec)
     :error-fn (fn [{:keys [msg]}]
                 (println msg)
                 (println)
                 (println "Usage: my-app [options]")
                 (println (cli/format-opts {:spec spec}))
                 (System/exit 1))}))

(when (= *file* (System/getProperty "babashka.file"))
  (apply -main *command-line-args*))

Subcommand CLI

#!/usr/bin/env bb

(ns my-cli
  (:require [babashka.cli :as cli]))

(defn build [{:keys [opts]}]
  (println "Building with options:" opts))

(defn test [{:keys [opts]}]
  (println "Running tests with options:" opts))

(defn help [_]
  (println "Commands: build, test"))

(def commands
  [{:cmds ["build"]
    :fn build
    :spec {:target {:coerce :keyword}
           :release {:coerce :boolean}}}
   {:cmds ["test"]
    :fn test
    :spec {:watch {:coerce :boolean}}}
   {:cmds []
    :fn help}])

(defn -main [& args]
  (cli/dispatch commands args))

(when (= *file* (System/getProperty "babashka.file"))
  (apply -main *command-line-args*))

Configuration File + CLI Override

(require '[clojure.edn :as edn])

(defn load-config [path]
  (when (.exists (io/file path))
    (edn/read-string (slurp path))))

(defn run [args]
  (let [file-config (load-config "config.edn")
        cli-opts (cli/parse-args args
                   {:coerce {:port :long
                             :workers :long}})
        final-config (merge file-config (:opts cli-opts))]
    ;; Use final-config
    ))

Babashka Task Integration

In bb.edn:

{:tasks
 {:requires ([babashka.cli :as cli])

  test {:doc "Run tests"
        :task (let [opts (cli/parse-opts *command-line-args*
                           {:coerce {:watch :boolean}})]
                (when (:watch opts)
                  (println "Running in watch mode"))
                (shell "clojure -M:test"))}}}

Long Option Syntax Variations

;; All equivalent
(cli/parse-opts ["--port" "8080"])
(cli/parse-opts ["--port=8080"])
(cli/parse-opts [":port" "8080"])

;; With coercion
(cli/parse-opts ["--port=8080"] {:coerce {:port :long}})
;;=> {:port 8080}

Repeated Options

;; Collect into vector
(cli/parse-opts ["--include" "*.clj" "--include" "*.cljs"]
  {:coerce {:include []}})
;;=> {:include ["*.clj" "*.cljs"]}

;; Count occurrences
(defn inc-counter [m k]
  (update m k (fnil inc 0)))

(cli/parse-opts ["-v" "-v" "-v"]
  {:collect {:v inc-counter}})
;;=> {:v 3}

Rest Arguments

Arguments after -- are collected as :rest-args:

(cli/parse-args ["--port" "8080" "--" "arg1" "arg2"]
  {:coerce {:port :long}})
;;=> {:opts {:port 8080}
;;    :rest-args ["arg1" "arg2"]}

Error Handling

Validation Failure Context

(defn validate-port [{:keys [value]}]
  (and (pos? value) (< value 65536)))

(cli/parse-args ["--port" "99999"]
  {:coerce {:port :long}
   :validate {:port {:pred validate-port
                      :ex-msg (fn [{:keys [option value]}]
                                (format "%s must be 1-65535, got %d"
                                        option value))}}})
;; Throws: :port must be 1-65535, got 99999

Graceful Degradation

(defn safe-parse [args]
  (try
    (cli/parse-args args {:coerce {:port :long}})
    (catch Exception e
      {:error (ex-message e)
       :opts {}})))

Exit Code Handling

(defn -main [& args]
  (let [result (cli/parse-args args
                 {:spec spec
                  :error-fn (fn [{:keys [msg]}]
                              (binding [*out* *err*]
                                (println "Error:" msg))
                              1)})]
    (if (number? result)
      (System/exit result)
      (do-work result))))

Use Cases

Build Tool CLI

(def build-commands
  [{:cmds ["compile"]
    :fn compile-project
    :spec {:target {:coerce :keyword
                    :desc "Compilation target"}
           :optimization {:coerce :keyword
                         :desc "Optimization level"}}}
   {:cmds ["package"]
    :fn package-project
    :spec {:format {:coerce :keyword
                    :desc "Package format"}}}])

Configuration Management

(defn read-env-config []
  (reduce-kv
   (fn [m k v]
     (if (str/starts-with? k "APP_")
       (assoc m (keyword (str/lower-case (subs k 4))) v)
       m))
   {}
   (System/getenv)))

(defn merged-config [args]
  (let [env-config (read-env-config)
        cli-config (:opts (cli/parse-args args))]
    (merge env-config cli-config)))

Testing Wrapper

(defn test-runner [{:keys [opts]}]
  (let [{:keys [namespace watch]} opts]
    (when watch
      (println "Starting test watcher..."))
    (apply clojure.test/run-tests
           (when namespace [(symbol namespace)]))))

(cli/dispatch
  [{:cmds ["test"]
    :fn test-runner
    :spec {:namespace {:desc "Specific namespace"}
           :watch {:coerce :boolean
                   :desc "Watch mode"}}}]
  *command-line-args*)

Performance Considerations

Minimize Parsing Overhead

For frequently called operations, parse once and pass options:

(defn process-files [opts files]
  (doseq [f files]
    (process-file f opts)))

(let [opts (cli/parse-args args)]
  (process-files (:opts opts) (:cmds opts)))

Coercion Functions

Custom coercion functions are called per-value:

;; Efficient: Use keywords for built-in types
{:coerce {:port :long}}

;; Less efficient: Custom function for simple types
{:coerce {:port #(Long/parseLong %)}}

Validation Overhead

Validators run after coercion. Use predicates wisely:

;; Good: Simple predicate
{:validate {:port pos?}}

;; Avoid: Complex validation in predicate
{:validate {:port (fn [p]
                     (and (pos? p)
                          (< p 65536)
                          (not (contains? reserved-ports p))))}}

Platform Notes

Babashka Integration

Since babashka 0.9.160, babashka.cli is built-in. Access via bb -x:

bb -x my-ns/my-fn :port 8080 :verbose true

Clojure CLI Integration

Use with -X flag:

clojure -X:my-alias my-ns/my-fn :port 8080

Add metadata to functions for specs:

(defn ^{:org.babashka/cli {:coerce {:port :long}}}
  start-server [opts]
  (println "Starting on port" (:port opts)))

JVM vs Native

babashka.cli works identically on JVM Clojure and native Babashka with minimal performance differences in parsing itself.

Cross-Platform Arguments

Quote handling varies by shell:

# Unix shells
script --name "My App"

# Windows cmd.exe
script --name "My App"

# PowerShell
script --name 'My App'

Use positional args to avoid quoting complexity:

script deploy production  # Better than: script :env "production"