piotr-yuxuan/malli-cli
Command-line interface with malli.
This library provides out-of-the-box command line interface. It
exposes a function that takes the args
vector of -main
and returns
a map representing the parsed, decoded arguments and environment
variables you are interested in.
The return map can be used as a config fragment, or overrides, that you can later merge with the config value provided by any other system. As such it intends to play nicely with configuration tools, so the actual configuration value of your program is a map that is a graceful merge of several overlapping config fragment:
The expected shape of your configuration being described as a malli schema so you can parse and decode strings as well as validating any constraints. It's quite powerful.
Semantic versioning is used, so no breaking changes will be introduced
without incrementing the major version. Some bug fixes may be
introduced but I currently don't plan to add any new feature. As
examplified belowe, malli-cli
should cover most of your use cases
with simplicity – or open an issue.
utility_name [-a][-b][-c option_argument]
[-d|-e][-f[option_argument]][operand...]
The utility in the example is named utility_name
. It is followed by
options, option-arguments, and operands. The arguments that consist of
-
characters and single letters or digits, such as a
, are known as
"options" (or, historically, "flags"). Certain options are followed by
an "option-argument", as shown with [ -c option_argument]
. The
arguments following the last options and option-arguments are named
"operands".
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, :decode/args-transformer malli-cli/args-transformer}
[:show-config? [boolean? {:optional true
:arg-number 0
:description "Print actual configuration value and exit."}]]
[:help [boolean? {:short-option "-h"
:optional true
:arg-number 0}]]
[:upload-api [string? {:short-option "-a"
:env-var "CORP_UPLOAD_API"
: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 malli-cli/non-idempotent-option
:default :error
:default->str name}
:off :fatal :error :warn :info :debug :trace :all]]
[:proxy [:map
[:host string?]
[:port pos-int?]]]]))
Here is the command summary produced by (malli-cli/summary)
for this
config:
Short Long option Default Description
--show-config Print actual configuration value and exit.
-h --help
-a --upload-api "http://localhost:8080" Address of target upload-api instance.
-v --log-level error
--proxy-host
--proxy-port
Let's try to call this program. You may invoke your Clojure main function with:
lein run \
--help -vvv \
-a "https://localhost:3004"
Let's suppose your configuration system provides this value:
{:proxy {:host "https://proxy.example.com"
:port 3128}}
and the shell environment variable CORP_UPLOAD_API
is set to
https://localhost:3004
. Then 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])
(require '[piotr-yuxuan.utils :refer [deep-merge]])
(defn load-config
[args]
(deep-merge
;; Value retrieved from any configuration system you want
(:value (configure {:key service-name
:env (env)
:version (version)}))
;; Command-line arguments, env-vars, and default values.
(m/decode Config args malli-cli/cli-transformer)))
(defn -main
[& args]
(let [config (load-config args)]
(cond (not (m/validate Config config))
(do (log/error "Invalid configuration value"
(m/explain Config config))
(Thread/sleep 60000) ; Leave some time to retrieve the logs.
(System/exit 1))
(:show-config? config)
(do (println config)
(System/exit 0))
(:help config)
(do (println (simple-summary Config))
(System/exit 0))
:else
(app/start config))))
See tests for minimal working code for each of these examples.
--long-option VALUE
may give{:long-option "VALUE"}
;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
[:long-option string?]]
--long-option=VALUE
may give{:long-option "VALUE"}
;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
[:long-option string?]]
-s VALUE
may give{:some-option "VALUE"}
;; Example schema:
[:map {:decode/args-transformer malli-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/args-transformer malli-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/args-transformer malli-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/args-transformer malli-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/args-transformer malli-cli/args-transformer}
[:log-level [:and
keyword?
[:enum {:short-option "-v"
:short-option/arg-number 0
:short-option/update-fn malli-cli/non-idempotent-option
:default :error}
:off :fatal :error :warn :info :debug :trace :all]]]]
[:map {:decode/args-transformer malli-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/args-transformer malli-cli/args-transformer}
[:proxy [:map
[:host string?]
[:port pos-int?]]]]
--upload-parallelism 32
may give:{:upload/parallelism 32}
;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
[:upload/parallelism pos-int?]]
--name Piotr
:{:vanity-name ">> Piotr <<"
:original-name "Piotr"
:first-letter \P}
;; Example schema:
[:map {:decode/args-transformer malli-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/args-transformer malli-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"]}
USER
set to piotr-yuxuan
may give:{:user "piotr-yuxuan"}
;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
[:user [string? {:env-var "USER"}]]]
Note that environment variables behave like default values with lower priority than command-line arguments. Env vars are resolved at decode time, not at schema compile time. This lack of purity is balanced by the environment being constant and set by the JVM at start-up time.
Can you improve this documentation? These fine people already did:
piotr-yuxuan, Burin Choomnuan & 胡雨軒 ПетрEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close