piotr-yuxuan/malli-cli
Command-line arguments with malli.
This library provides out-of-the-box cli parsing. It exposes a
function which takes a vector of strings args
as input and return a
map that can later be merged with the config. As such it intends to
play nicely with configuration tools, so you may have the following
workflow:
Let's consider this config schema:
(require '[piotr-yuxuan.malli-cli :as malli-cli])
(require '[malli.core :as m])
(def Config
(m/schema
[:map {:closed true}
[:help [boolean? {:short-option "-h"
:optional true
:arg-number 0}]]
[:upload-api [string?
{:short-option "-a"
:default "http://localhost:8080"
:description "Address of target upload-api instance."}]]
[:log-level [:enum {:decode/string keyword
:short-option "-v"
:short-option/arg-number 0
:short-option/update-fn (fn [options {:keys [path schema]} _cli-args]
(update-in options path (malli-cli/children-successor schema)))
:default :error}
:off :fatal :error :warn :info :debug :trace :all]]
[:proxy [:map
[:host string?]
[:port pos-int?]]]]))
Also, here is what your configuration system provides:
{:upload-api "https://example.com/upload"
:proxy {:host "https://proxy.example.com"
:port 3128}}
You may invoke your Clojure main function with:
lein run \
--help -vvv \
-a "https://localhost:3004"
and the resulting configuration passed to your app will be:
{:help true
:upload-api "https://localhost:3004"
:log-level :debug
:proxy {;; Nested config maps are supported
:host "http://proxy.example.com"
;; malli transform strings into appropriate types
:port 3128}
From a technical point of view, it leverages malli coercion and decoding capabilities so that you may define the shape of your configuration and default value in one place, then derive a command-line interface from it.
(require '[piotr-yuxuan.malli-cli :as malli-cli])
(require '[malli.core :as m])
(defn load-config
[args]
(deep-merge
;; Default value
(m/decode Config {} mt/default-value-transformer)
;; Value retrieved from configuration system
(:value (configure {:key service-name
:env (env)
:version (version)}))
;; Command-line overrides
(m/decode Config args malli-cli/simple-cli-options-transformer)))
(defn -main
[& args]
(let [config (load-config args)]
(if (m/validate Config config)
(app/start config)
(do (log/error "Invalid configuration value"
(m/explain Config config))
(Thread/sleep 60000) ; Leave some time to retrieve the logs.
(System/exit 1)))))
See tests for minimal working code for each of these examples.
--long-option VALUE
may give{:long-option "VALUE"}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:long-option string?]]
--long-option=VALUE
may give{:long-option "VALUE"}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:long-option string?]]
-s VALUE
may give{:some-option "VALUE"}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:some-option [string? {:short-option "-s"}]]]
-a -b val0 --c val1 val2
{:a true
:b "val0"
:c ["val1" "val2"]}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:a [boolean? {:arg-number 0}]]
[:b string?]
[:c [string? {:arg-number 2}]]]
-a 1 ARG0 -b 2 -- ARG1 ARG2
may be
equivalent to:{:a 1
:b 2
:piotr-yuxuan.malli-cli/arguments ["ARG0" "ARG1" "ARG2"]}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:a [boolean? {:arg-number 0}]]
[:b string?]]
-hal
are expanded like, for example:{:help true
:all true
:list true}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:help [boolean? {:short-option "-h" :arg-number 0}]]
[:all [boolean? {:short-option "-a" :arg-number 0}]]
[:list [boolean? {:short-option "-l" :arg-number 0}]]]
-vvv
are supported and may be rendered as:{:verbosity 3}
;; or, depending on what you want:
{:log-level :debug}
;; Example schemas:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:log-level [:and
keyword?
[:enum {:short-option "-v"
:short-option/arg-number 0
:short-option/update-fn (fn [options {:keys [in schema]} _cli-args]
(update-in options in (malli-cli/children-successor schema)))
:default :error}
:off :fatal :error :warn :info :debug :trace :all]]]]
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:verbosity [int? {:short-option "-v"
:short-option/arg-number 0
:short-option/update-fn (fn [options {:keys [in]} _cli-args]
(update-in options in (fnil inc 0)))
:default 0}]]]
--proxy-host https://example.org/upload --proxy-port 3447
is expanded as:{:proxy {:host "https://example.org/upload"
:port 3447}}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:proxy [:map
[:host string?]
[:port pos-int?]]]]
--upload-parallelism 32
may give:{:upload/parallelism 32}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:upload/parallelism pos-int?]]
--name Piotr
:{:vanity-name ">> Piotr <<"
:original-name "Piotr"
:first-letter \P}
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:vanity-name [string? {:long-option "--name"
:update-fn (fn [options {:keys [in]} [username]]
(-> options
(assoc :vanity-name (format ">> %s <<" username))
(assoc :original-name username)
(assoc :first-letter (first username))))}]]
[:original-name string?]
[:first-letter char?]]
-h --help nil
-a --upload-api "http://localhost:8080" Address of target upload-api instance.
-v --log-level :error
--proxy-host nil
--proxy-port nil
;; Example schema:
[:map {:decode/cli-args-transformer malli-cli/cli-args-transformer}
[:my-option string?]]
;; Example input:
["--unknown-long-option" "--other-option" "VALUE" "-s"}
;; Exemple output:
#:piotr-yuxuan.malli-cli{:unknown-option-errors ({:arg "-s"} {:arg "--other-option"} {:arg "--unknown-long-option"}),
:known-options ("--my-option"),
:arguments ["VALUE"],
:cli-args ["--unknown-long-option" "--other-option" "VALUE" "-s"]}
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close