Back to skills
SkillHub ClubShip Full StackFull Stack

babashka.cli

Command-line argument parsing for turning Clojure functions into CLIs

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3
Hot score
80
Updated
March 20, 2026
Overall rating
C1.9
Composite score
1.9
Best-practice grade
D47.9

Install command

npx @skill-hub/cli install hugoduncan-library-skills-babashka-cli

Repository

hugoduncan/library-skills

Skill path: plugins/clojure-libraries/skills/babashka-cli

Command-line argument parsing for turning Clojure functions into CLIs

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: hugoduncan.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install babashka.cli into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/hugoduncan/library-skills before adding babashka.cli to shared team environments
  • Use babashka.cli for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw 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`:
```clojure
{:deps {org.babashka/cli {:mvn/version "0.8.60"}}}
```

Or `bb.edn` for Babashka:
```clojure
{: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.

```clojure
(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.

```clojure
(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.

```clojure
(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:

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

Typed collections:

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

#### auto-coerce

Automatic coercion for unspecified options (enabled by default):

```clojure
(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

```clojure
;; 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:

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

#### Variable Length Collections

Use `repeat` for collecting remaining args:

```clojure
(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

```clojure
(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

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

#### Restricted Options

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

#### Custom Validators

```clojure
(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`:

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

### Error Handling

Custom error handler:

```clojure
(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:

```clojure
(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

```clojure
(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:

```clojure
(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:

```clojure
(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:

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

#### format-table

Format tabular data:

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

#### spec->opts

Convert spec to parse options:

```clojure
(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:

```clojure
(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

```clojure
#!/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

```clojure
#!/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

```clojure
(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`:

```clojure
{: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

```clojure
;; 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

```clojure
;; 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`:

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

## Error Handling

### Validation Failure Context

```clojure
(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

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

### Exit Code Handling

```clojure
(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

```clojure
(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

```clojure
(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

```clojure
(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:

```clojure
(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:

```clojure
;; 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:

```clojure
;; 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`:

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

### Clojure CLI Integration

Use with `-X` flag:

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

Add metadata to functions for specs:

```clojure
(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:

```bash
# 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:

```bash
script deploy production  # Better than: script :env "production"
```
babashka.cli | SkillHub