• Stars
    star
    153
  • Rank 235,358 (Top 5 %)
  • Language
    Clojure
  • License
    MIT License
  • Created almost 2 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Turn Clojure functions into CLIs!

babashka.cli

Clojars Project bb built-in

Turn Clojure functions into CLIs!

API

Status

This library is still in design phase and may still undergo breaking changes. Check breaking changes before upgrading!

Installation

Add to your deps.edn or bb.edn :deps entry:

org.babashka/cli {:mvn/version "0.4.39"}

Intro

Command line arguments in clojure and babashka CLIs are often in the form:

$ cli command :opt1 v1 :opt2 v2

or the more Unixy:

$ cli command --long-opt1 v1 -o v2

The main ideas:

  • Put as little effort as possible into turning a clojure function into a CLI, similar to -X style invocations. For lazy people like me! If you are not familiar with clj -X, read the docs here.
  • But with a better UX by not having to use quotes on the command line as a result of having to pass EDN directly: :dir foo instead of :dir '"foo"' or who knows how to write the latter in cmd.exe or Powershell.
  • Open world assumption: passing extra arguments does not break and arguments can be re-used in multiple contexts.
  • Because the line between calling functions from the command line and Clojure itself is blurred, validation of arguments should happen in your Clojure function, using your favorite tools (manually, spec, schema, malli...). As such, the library only focuses on coercion: turning argument strings into data which is then passed to your function.

Both : and -- are supported as the initial characters of a named option, but cannot be mixed. See options for more details.

See clojure CLI for how to turn your exec functions into CLIs.

Projects using babashka CLI

TOC

Options

For parsing options, use either parse-opts or parse-args.

Examples:

Parse {:port 1339} from command line arguments:

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

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

Use an alias (short option):

(cli/parse-opts ["-p" "1339"] {:alias {:p :port} :coerce {:port :long}})
;; {:port 1339}

Coerce values into a collection:

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

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

Transforming to a collection of a certain type:

(cli/parse-opts ["--foo" "bar" "--foo" "baz"] {:coerce {:foo [:keyword]}})
;; => {:foo [:bar :baz]}

Booleans need no explicit true value and :coerce option:

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

(cli/parse-opts ["-v" "-v" "-v"] {:alias {:v :verbose}
                                  :coerce {:verbose []}})
;;=> {:verbose [true true true]}

Long options also support the syntax --foo=bar:

(cli/parse-opts ["--foo=bar"])
;;=> {:foo "bar"}

Flags may be combined into a single short option (since 0.7.51):

(cli/parse-opts ["-abc"])
;;=> {:a true :b true :c true}

Arguments that start with --no- arg parsed as negative flags (since 0.7.51):

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

Auto-coercion

Since v0.3.35 babashka CLI auto-coerces values that have no explicit coercion with auto-coerce: it automatically tries to convert booleans, numbers and keywords.

Arguments

To parse positional arguments, you can use parse-args and/or the :args->opts option. E.g. to parse arguments for the git push command:

(cli/parse-args ["--force" "ssh://foo"] {:coerce {:force :boolean}})
;;=> {:args ["ssh://foo"], :opts {:force true}}

(cli/parse-args ["ssh://foo" "--force"] {:coerce {:force :boolean}})
;;=> {:args ["ssh://foo"], :opts {:force true}}

Note that this library can only disambiguate correctly between values for options and trailing arguments with enough :coerce information available. Without the :force :boolean info, we get:

(cli/parse-args ["--force" "ssh://foo"])
{:opts {:force "ssh://foo"}}

In case of ambiguity -- may also be used to communicate the boundary between options and arguments:

(cli/parse-args ["--paths" "src" "test" "--" "ssh://foo"] {:coerce {:paths []}})
{:args ["ssh://foo"], :opts {:paths ["src" "test"]}}

:args->opts

To fold positional arguments into the parsed options, you can use :args->opts:

(def cli-opts {:coerce {:force :boolean} :args->opts [:url]})

(cli/parse-opts ["--force" "ssh://foo"] cli-opts)
;;=> {:force true, :url "ssh://foo"}
(cli/parse-opts ["ssh://foo" "--force"] cli-opts)
;;=> {:url "ssh://foo", :force true}

If you want to fold a variable amount of arguments, you can coerce into a vector and specify the variable number of arguments with repeat:

(def cli-opts {:coerce {:bar []} :args->opts (cons :foo (repeat :bar))})
(cli/parse-opts ["arg1" "arg2" "arg3" "arg4"] cli-opts)
;;=> {:foo "arg1", :bar ["arg2" "arg3" "arg4"]}

Restrict

Use the :restrict option to restrict options to only those explicitly mentioned in configuration:

(cli/parse-args ["--foo"] {:restrict [:bar]})
;;=>
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:357).
Unknown option: :foo

Require

Use the :require option to throw an error when an option is not present:

(cli/parse-args ["--foo"] {:require [:bar]})
;;=>
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:363).
Required option: :bar

Validate

(cli/parse-args ["--foo" "0"] {:validate {:foo pos?}})
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:378).
Invalid value for option :foo: 0

To gain more control over the error message, use :pred and :ex-msg:

(cli/parse-args ["--foo" "0"] {:validate {:foo {:pred pos? :ex-msg (fn [m] (str "Not a positive number: " (:value m)))}}})
;;=>
Execution error (ExceptionInfo) at babashka.cli/parse-opts (cli.cljc:378).
Not a positive number: 0

Adding default args

You can supply default args with :exec-args:

(cli/parse-args ["--foo" "0"] {:exec-args {:bar 1}})
;;=> {:foo 0, :bar 1}

Note that args specified in args will override defaults in :exec-args:

(cli/parse-args ["--foo" "0" "--bar" "42"] {:exec-args {:bar 1}})
;;=> {:foo 0, :bar 42}

Error handling

By default, an exception will be thrown in the following situations:

  • A restricted option is encountered
  • A required option is missing
  • Validation fails for an option
  • Coercion fails for an option

You may supply a custom error handler function with :error-fn. The function will be called with a map containing the following keys:

  • :type - :org.babashka/cli (for filtering out other types of errors).
  • :cause - one of:
    • :restrict - a restricted option was encountered.
    • :require - a required option was missing.
    • :validate - validation failed for an option.
    • :coerce - coercion failed for an option.
  • :msg - default error message.
  • :option - the option being parsed when the error occurred.
  • :spec - the spec passed into parse-opts (see the Spec section).

The following keys are present depending on :cause:

  • :cause :restrict
    • :restrict - the value of the :restrict opt to parse-args (see the Restrict section).
  • :cause :require
    • :require - the value of the :require opt to parse-args (see the Require section).
  • :cause :validate
    • :value - the value of the option that failed validation.
    • :validate - the value of the :validate opt to parse-args (see the Validate section).
  • :cause :coerce
    • :value - the value of the option that failed coercion.

It is recommended to either throw an exception or otherwise exit in the error handler function, unless you want to collect all of the errors and act on them in the end (see babashka.cli-test/error-fn-test for an example of this).

For example:

(cli/parse-opts
 []
 {:spec {:foo {:desc "You know what this is."
         :ref "<val>"}}
  :error-fn
  (fn [{:keys [spec type cause msg option] :as data}]
    (if (= :org.babashka/cli type)
      (case cause
        :require
        (println
         (format "Missing required argument:\n%s"
                 (cli/format-opts {:spec (select-keys spec [option])})))
        (println msg))
      (throw (ex-info msg data)))
    (System/exit 1))})

would print:

Missing required argument:
  --foo <val> You know what this is.

Spec

This library can work with partial information to parse options. As such, the options to parse-opts and parse-args are optimized for terseness. However, when writing a CLI that supports automated printing of options, it is recommended to use the spec format:

(def spec {:from   {:ref          "<format>"
                    :desc         "The input format. <format> can be edn, json or transit."
                    :coerce       :keyword
                    :alias        :i
                    :default-desc "edn"
                    :default      :edn}
           :to     {:ref          "<format>"
                    :desc         "The output format. <format> can be edn, json or transit."
                    :coerce       :keyword
                    :alias        :o
                    :default-desc "json"
                    :default      :json}
           :pretty {:desc         "Pretty-print output."
                    :alias        :p}
           :paths  {:desc         "Paths of files to transform."
                    :coerce       []
                    :default      ["src" "test"]
                    :default-desc "src test"}})

You can pass the spec to parse-opts under the :spec key: (parse-opts args {:spec spec}). An explanation of each key:

  • :ref: a name which can be used as a reference in the description (:desc)
  • :desc: a description of the option.
  • :coerce: coerce string to given type.
  • :alias: mapping of short name to long name.
  • :default: default value.
  • :default-desc: a string representation of the default value.
  • :require: true make this opt required.
  • :validate: a function used to validate the value of this opt (as described in the Validate section).

Help

Given the above spec you can print options as follows:

(println (cli/format-opts {:spec spec :order [:from :to :paths :pretty]}))

This will print:

  -i, --from   <format> edn      The input format. <format> can be edn, json or transit.
  -o, --to     <format> json     The output format. <format> can be edn, json or transit.
      --paths           src test Paths of files to transform.
  -p, --pretty                   Pretty-print output.

As options can often be re-used in multiple subcommands, you can determine the order and selection of printed options with :order. If you don't want to use :order and simply want to present the options as written, you can also use a vector of vectors for the spec:

[[:pretty {:desc "Pretty-print output."
           :alias :p}]
 [:paths {:desc "Paths of files to transform."
          :coerce []
          :default ["src" "test"]
          :default-desc "src test"}]]

Subcommands

To handle subcommands, use dispatch.

An example. Say we want to create a CLI that can be called as:

$ example copy <file> --dry-run
$ example delete <file> --recursive --depth 3

This can be accomplished by doing the following:

(ns example
  (:require [babashka.cli :as cli]))

(defn copy [m]
  (assoc m :fn :copy))

(defn delete [m]
  (assoc m :fn :delete))

(defn help [m]
  (assoc m :fn :help))

(def table
  [{:cmds ["copy"]   :fn copy   :args->opts [:file]}
   {:cmds ["delete"] :fn delete :args->opts [:file]}
   {:cmds []         :fn help}])

(defn -main [& args]
  (cli/dispatch table args {:coerce {:depth :long}}))

Calling the example namespace's -main function can be done using clojure -M -m example or bb -m example. The last entry in the dispatch-table always matches and calls the help function.

When running clj -M -m example --help, dispatch calls help which returns:

{:opts {:help true}, :dispatch [], :fn :help}

When running clj -M -m example copy the-file --dry-run, dispatch calls copy, which returns:

{:cmds ["copy" "the-file"], :opts {:file "the-file" :dry-run true},
 :dispatch ["copy"], :fn :copy}

When running clj -M -m example delete the-file --depth 3, dispatch calls delete which returns:

{:cmds ["delete" "the-file"], :opts {:depth 3, :file "the-file"},
 :dispatch ["delete"], :fn :delete}

See neil for a real world example of a CLI that uses subcommands.

Additional parse-arg options may be passed in each table entry:

(def table
  [{:cmds ["copy"]   :fn copy   :args->opts [:file] :alias {:f :file :restrict true}}
   {:cmds ["delete"] :fn delete :args->opts [:file]}
   {:cmds []         :fn help}])

Babashka tasks

For documentation on babashka tasks, go here.

Since babashka 0.9.160, babashka.cli has become a built-in and has better integration through -x and exec. Read about that in the babashka book.

Clojure CLI

You can control parsing behavior by adding :org.babashka/cli metadata to Clojure functions. It does not introduce a dependency on babashka.cli itself. Not adding any metadata will result in string values, which in many cases may already be a reasonable default.

Adding support for this library will cause less friction with shell usage, especially on Windows since you need less quoting. You can support the same function for both clojure -X and clojure -M style invocations without writing extra boilerplate.

In your deps.edn :aliases entry, add:

:exec {:extra-deps {org.babashka/cli {:mvn/version "0.4.39"}}
       :main-opts ["-m" "babashka.cli.exec"]}

Now you can call any function that accepts a map argument. E.g.:

$ clojure -M:exec clojure.core prn :a 1 :b 2
{:a "1", :b "2"}

Use :org.babashka/cli metadata for coercions:

(ns my-ns)

(defn foo
  {:org.babashka/cli {:coerce {:a :symbol
                               :b :long}}}
  ;; map argument:
  [m]
  ;; print map argument:
  (prn m))
$ clojure -M:exec my-ns foo :a foo/bar :b 2 :c vanilla
{:a foo/bar, :b 2, :c "vanilla"}

Note that any library can add support for babashka CLI without depending on babashka CLI.

An example that specializes babashka.cli usage to a function:

:prn {:extra-deps {org.babashka/cli {:mvn/version "0.4.39"}}
      :main-opts ["-m" "babashka.cli.exec" "clojure.core" "prn"]}
$ clojure -M:prn --foo=bar --baz
{:foo "bar" :baz true}

You can also pre-define the exec function in :exec-fn:

:prn {:extra-deps {org.babashka/cli {:mvn/version "0.4.39"}}
      :exec-fn clojure.core/prn
      :main-opts ["-m" "babashka.cli.exec"]}

To alter the parsing behavior of functions you don't control, you can add :org.babashka/cli data in the deps.edn alias:

:prn {:deps {org.babashka/cli {:mvn/version "0.4.39"}}
      :exec-fn clojure.core/prn
      :main-opts ["-m" "babashka.cli.exec"]
      :org.babashka/cli {:coerce {:foo :long}}}
$ clojure -M:prn --foo=1
{:foo 1}

antq

.clojure/deps.edn alias:

:antq {:deps {org.babashka/cli {:mvn/version "0.4.39"}
              com.github.liquidz/antq {:mvn/version "1.7.798"}}
       :paths []
       :main-opts ["-m" "babashka.cli.exec" "antq.tool" "outdated"]
       :org.babashka/cli {:coerce {:skip []}}}

On the command line you can now run it with:

$ clj -M:antq --upgrade

Note that we are calling the same outdated function that you normally call with -T:

$ clj -Tantq outdated :upgrade true

even though antq has its own -main function.

Note that we added the :org.babashka/cli {:coerce {:skip []}} data in the alias to make sure that --skip options get collected into a vector:

clj -M:antq --upgrade --skip github-action

vs.

clj -Tantq outdated :upgrade true :skip '["github-action"]'

The following projects have added support for babashka CLI. Feel free to add a PR to list your project as well!

clj-new

codox

In deps.edn create an alias:

:codox {:extra-deps {org.babashka/cli {:mvn/version "0.4.39"}
                     codox/codox {:mvn/version "0.10.8"}}
        :exec-fn codox.main/generate-docs
        ;; default arguments:
        :exec-args {:source-paths ["src"]}
        :org.babashka/cli {:coerce {:source-paths []
                                    :doc-paths []
                                    :themes [:keyword]}}
        :main-opts ["-m" "babashka.cli.exec"]}

CLI invocation:

$ clojure -M:codox --output-path /tmp/out

deps-new

kaocha

In deps.edn create an alias:

:kaocha {:extra-deps {org.babashka/cli {:mvn/version "0.4.39"}
                      lambdaisland/kaocha {:mvn/version "1.66.1034"}}
         :exec-fn kaocha.runner/exec-fn
         :exec-args {} ;; insert default arguments here
         :org.babashka/cli {:alias {:watch :watch?
                                      :fail-fast :fail-fast?}
                            :coerce {:skip-meta :keyword
                                     :kaocha/reporter [:symbol]}}
         :main-opts ["-m" "babashka.cli.exec"]}

Now you are able to use kaocha's exec-fn to be used as a CLI:

$ clj -M:kaocha --watch --fail-fast --kaocha/reporter kaocha.report/documentation

quickdoc

tools.build

In deps.edn create an alias:

:build {:deps {org.babashka/cli {:mvn/version "0.4.39"}
               io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
        :paths ["."]
        :ns-default build
        :main-opts ["-m" "babashka.cli.exec"]}

Now you can call your build functions as CLIs:

clj -M:build jar --verbose

tools.deps.graph

In deps.edn create an alias:

:graph {:deps {org.babashka/cli {:mvn/version "0.4.39"}
               org.clojure/tools.deps.graph {:mvn/version "1.1.68"}}
        :exec-fn clojure.tools.deps.graph/graph
        :exec-args {} ;; insert default arguments here
        :org.babashka/cli {:coerce {:trace-omit [:symbol]}}
        :main-opts ["-m" "babashka.cli.exec"]}

Then invoke on the command line:

clj -M:graph --size --output graph.png

Leiningen

This tool can be used to run clojure exec functions with lein.

An example with clj-new:

In ~/.lein/profiles.clj put:

{:clj-1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]}
 :clj-new {:dependencies [[org.babashka/cli "0.4.39"]
                          [com.github.seancorfield/clj-new "1.2.381"]]}
 :user {:aliases {"clj-new" ["with-profiles" "+clj-1.11,+clj-new"
                             "run" "-m" "babashka.cli.exec"
                             {:exec-args {:env {:description "My project"}}
                              :coerce {:verbose :long
                                       :args []}
                              :alias {:f :force}}
                             "clj-new"]}}}

After that you can use lein clj-new app to create a new app:

$ lein clj-new app --name foobar/baz --verbose 3 -f

License

Copyright © 2022 Michiel Borkent

Distributed under the MIT License. See LICENSE.

More Repositories

1

babashka

Native, fast starting Clojure interpreter for scripting
Clojure
3,642
star
2

sci

Configurable Clojure/Script interpreter suitable for scripting and Clojure DSLs
Clojure
1,167
star
3

nbb

Scripting in Clojure on Node.js using SCI
Clojure
772
star
4

neil

A CLI to add common aliases and features to deps.edn-based projects
Clojure
288
star
5

scittle

Execute Clojure(Script) directly from browser script tags via SCI
Clojure
278
star
6

obb

Ad-hoc ClojureScript scripting of Mac applications via Apple's Open Scripting Architecture.
Clojure
236
star
7

process

Clojure library for shelling out / spawning sub-processes
Clojure
183
star
8

fs

File system utility library for Clojure
Clojure
139
star
9

babashka.curl

A This library is mostly replaced by https://github.com/babashka/http-client
Clojure
119
star
10

bbin

Install any Babashka script or project with one command
Clojure
115
star
11

pods

Pods support for JVM and babashka
Clojure
100
star
12

http-client

HTTP client for Clojure and Babashka built on java.net.http
Clojure
83
star
13

pod-registry

Pod manifests describe where pods can be downloaded, etc.
Clojure
82
star
14

babashka-sql-pods

Babashka pods for SQL databases
Clojure
72
star
15

pod-babashka-aws

Deprecated, use https://github.com/grzm/awyeah-api
Clojure
59
star
16

http-server

Serve static assets
Clojure
58
star
17

book

Babashka book
Clojure
55
star
18

babashka.nrepl

The nREPL server from babashka as a library, so it can be used from other SCI-based CLIs.
Clojure
50
star
19

pod-babashka-etaoin

Deprecated, use https://github.com/clj-commons/etaoin.
Clojure
41
star
20

tools-deps-native

Run tools.deps as a native binary
Clojure
39
star
21

pod-babashka-go-sqlite3

A babashka pod for interacting with sqlite3.
Go
27
star
22

pod-babashka-filewatcher

Babashka filewatcher pod based on Rust notify
Shell
22
star
23

json

JSON abstraction library
Clojure
19
star
24

pod-babashka-fswatcher

Babashka filewatcher pod.
Go
18
star
25

sci.configs

A collection of ready to be used SCI configs
Clojure
17
star
26

instaparse-bb

Use instaparse from babashka
Clojure
16
star
27

pod-babashka-buddy

A pod around buddy core (Cryptographic Api for Clojure).
Clojure
14
star
28

nbb-features

A collection of premade features for nbb
Clojure
10
star
29

toolbox

Script and template for Babashka Toolbox
HTML
9
star
30

pod-babashka-lanterna

Clojure
9
star
31

pod-babashka-malli

Exposing malli to babashka via a pod
Shell
8
star
32

nrepl-client

Clojure
8
star
33

pod-babashka-parcera

A babashka pod wrapping parcera
Clojure
7
star
34

conf

babashka-conf
Clojure
7
star
35

babashka.core

Babashka core utils
Clojure
5
star
36

pod-babashka-instaparse

Instaparse pod
Clojure
4
star
37

sci.nrepl

Clojure
2
star
38

babashka-dev-builds

2
star
39

.build

[INTERNAL] Common tooling for build orchestration
Clojure
1
star
40

babashka.github.io

Clojure
1
star
41

homebrew-brew

Ruby
1
star