Babashka CLI

repository·main·Indexed 43 days ago

https://github.com/babashka/cli

A library for turning Clojure functions into command line interfaces (CLIs). It supports Unix-style arguments, validation, type coercion, and subcommands. It is available for Clojure, ClojureScript, ClojureDart, Babashka, and JavaScript (via @babashka/cli version 0.12.91). Key features include a spec-based configuration for options, support for positional arguments, and a dispatch system for handling command hierarchies and automatic help generation.

Tokens
18.4K
Snippets
67
Records
78
Agent score
47%

What's inside Babashka CLI

  1. Configure options using a Spec

    main

    Options in Babashka CLI are configured using a spec (options specification). A spec is a map where keys are option names and values are maps defining the option's behavior.

    Key configuration options include:

    • :coerce: Defines how to transform the string input into a specific type (e.g., :long, :keyword, :boolean, or a custom function).
    • :alias: Provides a short-form name (e.g., :p for --port).
    • :desc: A description used for generating help text.
    • :require: If true, the option must be present.
    • :default: The value used if the option is omitted.
    • :negatable: If true, allows --no- prefix for boolean flags.
    • :validate: A function to validate the coerced value.

    You can use a full spec shape or a terser shape (lifting keys to the top level), but only the full spec shape supports :desc and :ref for help generation.

    {:spec {:port {:coerce :long :alias :p}}}
  2. Show allowed choices in --help output

    main

    The babashka.cli library automatically includes allowed values in the --help documentation for arguments and options.

    • For values defined via :validate (sets), the help text will show the values in sorted order.
    • For values defined via :enum (vectors), the help text will show the values in the declared order.

    The format used is (one of: val1, val2, ...).

    Arguments:
      <environment>  Target environment (one of: dev, staging, prod)
  3. Define multi-word commands and hierarchies

    main

    Multi-word commands (like git remote add) are handled by nesting commands. In a tree, this is done by nesting :cmd maps. In a table, this is done by providing the full path in the :cmds vector.

    When matching, Babashka CLI selects the longest matching entry. For example, if you have both remote and remote add, the command remote add will trigger the remote add function.

    Option Inheritance in Hierarchies:

    • By default, options are parsed using the spec of the current matching command. Specs are not merged across levels.
    • To allow a command to accept an option defined in a parent command, mark that option with :inherit true in the parent's spec.
    • Alternatively, you can pass :inherit to cli/dispatch:
      • true: Inherit all options from parents.
      • #{:key}: Inherit only specific option keys.
    ;; Tree hierarchy example
    {:cmd {"remote" 
           {:fn remotes-list :doc "show list of remotes"
            :cmd {"add"    {:fn remote-add :doc "add a new remote"}
                  "delete" {:fn remote-delete :doc "delete a remote"}}
            :cmd-order ["add" "delete"]}}}
    
    ;; Table hierarchy example
    [{:cmds ["remote"]          :fn remotes-list  :doc "show list of remotes"}
     {:cmds ["remote" "add"]    :fn remote-add    :doc "add a new remote"}
     {:cmds ["remote" "delete"] :fn remote-delete :doc "delete a remote"}]
  4. Handle boolean flags and negation

    main

    Boolean flags are assumed by default. If an option is present without a value, it is treated as true.

    • Default behavior: --verbose becomes {:verbose true}.
    • Explicit boolean: Use :coerce :boolean to be explicit.
    • Negation: Long options starting with --no- (e.g., --no-colors) are parsed as negative flags (e.g., {:colors false}). To make this visible in generated help, set :negatable true in the spec.
    • Multiple flags: If you use :coerce [] for a boolean, multiple short flags (like -vvv) will collect into a vector of booleans (e.g., [true true true]).
    ;; Default boolean
    (cli/parse-opts ["--verbose"])
    ;;=> {:verbose true}
    
    ;; Explicit boolean
    (cli/parse-opts ["--verbose"] {:spec {:verbose {:coerce :boolean}}})
    ;;=> {:verbose true}
    
    ;; Collecting multiple flags into a vector
    (cli/parse-opts ["-v" "-v" "-v"] {:spec {:verbose {:alias :v :coerce []}}})
    ;;=> {:verbose [true true true]}
  5. Use auto-coercion for simple types

    main

    If no explicit :coerce is provided in the spec, Babashka CLI uses auto-coerce to attempt to convert string values into booleans, numbers, or keywords.

    (cli/parse-opts ["--num" "1339" "--kw" ":foo" "--bool" "false" "--str" "bar"])
    ;; => {:num 1339, :kw :foo, :bool false, :str "bar"}
  6. Fold positional arguments into options using `:args->opts`

    main

    The :args->opts configuration allows you to map positional arguments directly into the :opts map returned by the parser.

    • Single argument: Pass a keyword to map the first positional argument to that key.
    • Variadic arguments: Use repeat within a cons to map a variable number of arguments to a vector.
    • Interspersed options: Options can appear before, between, or after positional arguments when using :args->opts.

    When using command dispatch, :args->opts allows the parser to continue looking for options even after positional arguments are encountered. Without it, dispatch stops at the first positional argument.

    ;; Map a single argument to :url
    (def cli-opts {:spec {:force {:coerce :boolean}} :args->opts [:url]})
    (cli/parse-opts ["--force" "ssh://foo"] cli-opts)
    ;;=> {:force true, :url "ssh://foo"}
    
    ;; Map multiple arguments to a vector using repeat
    (def cli-opts {:spec {:bar {:coerce []}} :args->opts (cons :foo (repeat :bar))})
    (cli/parse-opts ["arg1" "arg2" "arg3" "arg4"] cli-opts)
    ;;=> {:foo "arg1", :bar ["arg2" "arg3" "arg4"]}
  7. Configure type coercion for Clojure functions

    main

    To ensure command-line arguments are coerced into the correct types (e.g., :long, :symbol, or collections), add the :org.babashka/cli metadata to your function definition. This does not require your library to depend on babashka.cli itself.

    Example of defining a function with coercion:

    (ns my-ns)
    
    (defn foo
      {:org.babashka/cli {:coerce {:a :symbol
                                   :b :long}}}
      [m]
      (prn m))

    When called via the :exec alias, arguments will be parsed correctly: clojure -M:exec my-ns foo :a foo/bar :b 2 results in {:a foo/bar, :b 2}.

    (defn foo
      {:org.babashka/cli {:coerce {:a :symbol
                                   :b :long}}}
      [m]
      (prn m))
  8. Configure option value completions

    main

    You can define how option values are completed in your spec using several mechanisms. If no mechanism is provided, the shell defaults to file completion. To opt out of file completion, use :complete false.

    Supported methods:

    • :complete: A static collection of values or a map of {:value .. :description ..}.
    • :enum: Allowed values in the declared order.
    • :validate: A set-valued validation; the members of the set are used as sorted completions.
    • :complete-fn: A dynamic function for complex logic.

    Dynamic Completion Function Signature The :complete-fn receives a map: {:to-complete <partial> :opts <opts parsed so far> :option <key>}. It should return strings or {:value .. :description ..} maps. The library automatically applies prefix-filtering against the partial value.

    Positional Arguments Positional arguments mapped with :args->opts complete using the same logic as options. A positional argument resolves to its spec key by position.

    {:env   {:coerce :string
             :complete ["dev" "staging" "prod"]}
     :mode  {:enum ["fast" "safe"]}
     :level {:coerce :keyword
             :validate #{:local :global :system}}
     :branch {:coerce :string
              :complete-fn (fn [{:keys [to-complete opts]}] ; dynamic
                              (git-branches to-complete))}}
  9. Proposed recursive subcommand structure for babashka.cli

    main

    A proposal exists to support a recursive subcommand shape for babashka.cli to complement the existing flat table format. This structure allows for nested commands where every node follows a uniform shape. In this model, :spec defines the options for the current level, and :subcommands contains a vector of child command nodes.

    Note: This is a proposed structure and its implementation status was marked as 'OPEN' as of 2026-06-11. The flat table format remains the canonical method for programmatic generation and multi-file registration.

    {:spec {:format {:desc "edn or table"}}        ; this level's options
     :subcommands [{:cmd "outdated"
                    :fn  deps/outdated}
                   {:cmd "cache"
                    :doc "Manage cache"
                    :subcommands [{:cmd "clean"
                                   :fn  deps/clean-cache}]}]}
  10. Define commands using the recursive tree format

    main

    As of version 0.11.71, babashka.cli supports a recursive tree structure for command definitions. Instead of a flat list of paths, you can nest commands under a :cmd key. This format allows group documentation to live directly on the group node and avoids repeating parent command names in every descendant.

    Important Note on Ordering: Because Clojure maps do not guarantee order for more than 8 entries, you should use the :cmd-order key to explicitly define the display order of subcommands if your group contains many commands.

    {:doc  "Dependency tools"
     :spec {:format {:desc "edn or table"}}
     :cmd  {"outdated" {:fn deps/outdated}
            "cache"    {:doc "Manage cache"
                        :cmd {"clean" {:fn deps/clean-cache}}}}}
  11. Create a standalone CLI executable

    main

    To hide the launching dialect (like bb or clojure) and support shell completions, you can package your CLI as a standalone program. Common techniques include:

    • Shebang on script: Use #!/usr/bin/env bb at the top of your script (Supported by macOS, Linux, and Babashka).
    • Wrapper shell script: A shell-specific script that launches your CLI (Supported by macOS, Linux, and Windows).
    • bbin: An elegant way to distribute CLIs created with Babashka.
    • GraalVM native image: Compiles Clojure CLIs to OS-specific executables.
    • npm: Distribute via npm for global installation (npm install -g).
  12. Define single-word commands using tree or table formats

    main

    Babashka CLI allows you to define commands using either a nested tree structure or a flat table structure.

    • Tree format: Uses a :cmd map where keys are command names and values are maps containing the command's configuration (function, documentation, arguments, and sub-commands).
    • Table format: Uses a vector of maps where each map defines a command via the :cmds key (a vector of strings representing the command path).

    Each command entry can include:

    • :fn: The function to call when the command is matched.
    • :doc: Documentation string. The first line is used as a summary.
    • :args->opts: A vector of keys to map positional arguments into the :opts map.
    • :spec: The option specification for that command.
    • :no-doc: Set to true to hide the command from help output and completions.
    • :cmd-aliases: A vector of alternative names for the command.
    • :cmd-order: A vector of strings to specify the display order of commands in help output.
    ;; Tree format example
    (def tree
      {:cmd {"copy"   {:fn copy :doc "Copy a file\nMore details here" :args->opts [:file] 
                       :spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
             "delete" {:fn delete :doc "Delete a file" :args->opts [:file] 
                       :spec {:recursive {:coerce :boolean :desc "Recurse"}
                              :depth     {:coerce :long    :desc "Max depth"}}}}}
    
    ;; Table format example
    (def table
      [{:cmds ["copy"]   :fn copy   :doc "Copy a file" :args->opts [:file]
        :spec {:dry-run {:coerce :boolean :desc "Do a dry run"}}}
       {:cmds ["delete"] :fn delete :doc "Delete a file" :args->opts [:file]
        :spec {:recursive {:coerce :boolean :desc "Recurse"}
               :depth     {:coerce :long    :desc "Max depth"}}}])