{"id":20932832,"url":"https://github.com/piotr-yuxuan/malli-cli","last_synced_at":"2025-05-13T20:30:49.114Z","repository":{"id":37396369,"uuid":"373318234","full_name":"piotr-yuxuan/malli-cli","owner":"piotr-yuxuan","description":"Configuration powertool with `metosin/malli`","archived":false,"fork":false,"pushed_at":"2025-05-12T18:48:31.000Z","size":276,"stargazers_count":56,"open_issues_count":19,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-12T19:53:11.298Z","etag":null,"topics":["args-parser","babashka","bb","cli","cli-app","clojure","command-line","command-line-parser","command-line-tool","configuration","configuration-management","env-var","environment-variable","graalvm","malli","secret","secret-management","sourcing"],"latest_commit_sha":null,"homepage":"https://github.com/piotr-yuxuan/malli-cli","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"eupl-1.2","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/piotr-yuxuan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS.yml","security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["piotr-yuxuan"]}},"created_at":"2021-06-02T22:25:52.000Z","updated_at":"2025-05-12T18:49:18.000Z","dependencies_parsed_at":"2024-06-03T21:28:41.725Z","dependency_job_id":"35cce5ed-fe7d-4daa-a92f-3c18b5b0d1fb","html_url":"https://github.com/piotr-yuxuan/malli-cli","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piotr-yuxuan%2Fmalli-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piotr-yuxuan%2Fmalli-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piotr-yuxuan%2Fmalli-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/piotr-yuxuan%2Fmalli-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/piotr-yuxuan","download_url":"https://codeload.github.com/piotr-yuxuan/malli-cli/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254020902,"owners_count":22000805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["args-parser","babashka","bb","cli","cli-app","clojure","command-line","command-line-parser","command-line-tool","configuration","configuration-management","env-var","environment-variable","graalvm","malli","secret","secret-management","sourcing"],"created_at":"2024-11-18T21:53:32.159Z","updated_at":"2025-05-13T20:30:48.605Z","avatar_url":"https://github.com/piotr-yuxuan.png","language":"Clojure","funding_links":["https://github.com/sponsors/piotr-yuxuan"],"categories":["Terminal UI"],"sub_categories":[],"readme":"# `piotr-yuxuan/malli-cli`\n\n![](./doc/controller.jpg)\n\nConfiguration powertool with `metosin/malli`.\n\n\n[![Build Status](https://github.com/piotr-yuxuan/malli-cli/actions/workflows/walter-cd.yml/badge.svg)](https://github.com/piotr-yuxuan/malli-cli/actions/workflows/walter-cd.yml)\n[![Clojars badge](https://img.shields.io/clojars/v/com.github.piotr-yuxuan/malli-cli.svg)](https://clojars.org/com.github.piotr-yuxuan/malli-cli)\n[![Clojars downloads](https://img.shields.io/clojars/dt/com.github.piotr-yuxuan/malli-cli)](https://clojars.org/com.github.piotr-yuxuan/malli-cli)\n[![cljdoc badge](https://cljdoc.org/badge/com.github.piotr-yuxuan/malli-cli)](https://cljdoc.org/d/com.github.piotr-yuxuan/malli-cli/CURRENT)\n[![GitHub license](https://img.shields.io/github/license/piotr-yuxuan/malli-cli)](https://github.com/piotr-yuxuan/malli-cli/blob/main/LICENSE)\n[![GitHub issues](https://img.shields.io/github/issues/piotr-yuxuan/malli-cli)](https://github.com/piotr-yuxuan/malli-cli/issues)\n\u003ca href=\"https://babashka.org\" rel=\"nofollow\"\u003e\u003cimg src=\"https://github.com/babashka/babashka/raw/master/logo/badge.svg\" alt=\"bb compatible\" style=\"max-width: 100%;\"\u003e\u003c/a\u003e\n\n# What it offers\n\nThis library provides out-of-the-box:\n\n- Configuration sourcing from command line arguments, environment variables, or\n  any other configuration management tool you want to integrate with;\n- Very expressive means to describe the configuration you expect;\n- An agnostic way to protect secrets in your config that turn them into opaque,\n  non-printable values, while still being able to decode them and access the\n  original value when need be;\n- Seamless and powerful CLI generation based on your config schema, returns a\n  map representing the parsed, decoded arguments and environment variables you\n  are interested in.\n\nThe returned map can be used as a config fragment, that you can later merge with\nthe config value provided by any other system. As such it intends to play nicely\nwith configuration tools, so the actual configuration value of your program is a\nmap that is a graceful merge of several overlapping config fragment:\n\n1. Default configuration value;\n2. Environment variables when the program starts up;\n3. Value from some configuration management system;\n4. Command line arguments.\n\nThe expected shape of your configuration being described as a malli schema so\nyou can parse and decode strings as well as validating any constraints. It's\nquite powerful.\n\n# Maturity and evolution\n\nBreak versioning is used, so no breaking changes will be introduced without\nincrementing the major version. Some bug fixes may be introduced and I will keep\nadding new features as I encounter new needs. As illustrated below, `malli-cli`\nshould already cover most of your use cases with simplicity – or open an issue.\n\n# Naming\n\n``` txt\nutility_name [-a][-b][-c option_argument]\n             [-d|-e][-f[option_argument]][operand...]\n```\n\nThe utility in the example is named `utility_name`. It is followed by options,\noption-arguments, and operands. The arguments that consist of\n`-` characters and single letters or digits, such as `a`, are known as\n\"options\" (or, historically, \"flags\"). Certain options are followed by an \"\noption-argument\", as shown with `[ -c option_argument ]`. The arguments\nfollowing the last options and option-arguments are named\n\"operands\".\n\n# Simple example\n\nLet's consider this config schema:\n\n``` clojure\n(require '[piotr-yuxuan.malli-cli :as malli-cli])\n(require '[malli.core :as m])\n\n(def Config\n  (m/schema\n    [:map {:closed true, :decode/args-transformer malli-cli/args-transformer}\n     [:show-config? {:optional true}\n      [boolean? {:description \"Print actual configuration value and exit.\"\n                 :arg-number 0}]]\n     [:help {:optional true}\n      [boolean? {:description \"Display usage summary and exit.\"\n                 :short-option \"-h\"\n                 :arg-number 0}]]\n     [:upload-api [string? {:description \"Address of target upload-api instance. If not set from the command line, lookup env var $CORP_UPLOAD_API.\"\n                            :short-option \"-a\"\n                            ;; Cli option will be looked up, then env var, then default.\n                            :env-var \"CORP_UPLOAD_API\"\n                            :default \"http://localhost:8080\"}]]\n     [:log-level [:enum {:description \"Non-idempotent -v increases log level, --log-level sets it.\"\n                         ;; Express how to decode a string into an enum value.\n                         :decode/string keyword\n                         :short-option \"-v\"\n                         :short-option/arg-number 0\n                         :short-option/update-fn malli-cli/non-idempotent-option\n                         :default :error\n                         ;; Used in summary to pretty-print the default value to a string.\n                         :default-\u003estr name}\n                  :off :fatal :error :warn :info :debug :trace :all]]\n     [:proxy [:map\n              [:host string?]\n              ;; malli will parse and return an integer.\n              [:port pos-int?]]]]))\n```\n\nHere is the command summary produced by `(malli-cli/summary Config)`\nfor this config:\n\n``` txt\n  Short  Long option    Default                  Description\n         --show-config                           Print actual configuration value and exit.\n  -h     --help                                  Display usage summary and exit.\n  -a     --upload-api   \"http://localhost:8080\"  Address of target upload-api instance. If not set from the command line, lookup env var $CORP_UPLOAD_API.\n  -v     --log-level    error                    Non-idempotent -v increases log level, --log-level sets it.\n         --proxy-host\n         --proxy-port\n```\n\nLet's try to call this program (code details below). You may invoke your Clojure\nmain function with:\n\n``` zsh\nlein run \\\n  --help -vvv \\\n  -a \"https://localhost:4000\"\n```\n\nLet's suppose your configuration system provides this value:\n\n``` clojure\n{:proxy {:host \"https://proxy.example.com\"\n         :port 3128}}\n```\n\nand the shell environment variable `CORP_UPLOAD_API` is set to\n`https://localhost:7000`. Then the resulting configuration passed to your app\nwill be:\n\n``` clojure\n{:help true\n :upload-api \"https://localhost:4000\"\n :log-level :debug\n :proxy {;; Nested config maps are supported\n         :host \"http://proxy.example.com\"\n         ;; malli transform strings into appropriate types\n         :port 3128}\n```\n\nLet's try another time with this command with same provided config and env vars:\n\n``` zsh\nlein run \\\n  --log-level=off\n  --show-config\n```\n\nThe program will exit after printing:\n\n``` clojure\n{:log-level :off,\n :show-config? true,\n :upload-api \"http://localhost:7000\"}\n```\n\n---\n\nFrom a technical point of view, it leverages malli coercion and decoding\ncapabilities so that you may define the shape of your configuration and default\nvalue in one place, then derive a command-line interface from it.\n\n``` clojure\n(require '[piotr-yuxuan.malli-cli :as malli-cli])\n(require '[malli.core :as m])\n(require '[clojure.pprint])\n(require '[piotr-yuxuan.malli-cli.utils :refer [deep-merge]])\n\n(defn load-config\n  [args]\n  (deep-merge\n    ;; Value retrieved from any configuration system you want\n    (:value (configure {:key service-name\n                        :env (env)\n                        :version (version)}))\n    ;; Command-line arguments, env-vars, and default values.\n    (m/decode Config args malli-cli/cli-transformer)))\n\n(defn -main\n  [\u0026 args]\n  (let [config (load-config args)]\n    (cond (not (m/validate Config config))\n          (do (log/error \"Invalid configuration value\"\n                         (m/explain Config config))\n              (Thread/sleep 60000) ; Leave some time to retrieve the logs.\n              (System/exit 1))\n\n          (:show-config? config)\n          (do (clojure.pprint/pprint config)\n              (System/exit 0))\n\n          (:help config)\n          (do (println (simple-summary Config))\n              (System/exit 0))\n\n          :else\n          (app/start config))))\n```\n\n# Capabilities\n\nSee tests for minimal working code for each of these examples.\n\n- Long option flag and value `--long-option VALUE` may give\n\n``` clj\n{:long-option \"VALUE\"}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n  [:long-option string?]]\n```\n\n- Grouped flag and value with `--long-option=VALUE` may give\n\n``` clj\n{:long-option \"VALUE\"}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n  [:long-option string?]]\n```\n\n- Short option names with `-s VALUE` may give\n\n``` clj\n{:some-option \"VALUE\"}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:some-option [string? {:short-option \"-s\"}]]]\n```\n\n- Options that accept a variable number of arguments: `-a -b val0 --c val1 val2`\n\n``` clj\n{:a true\n :b \"val0\"\n :c [\"val1\" \"val2\"]}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:a [boolean? {:arg-number 0}]]\n [:b string?]\n [:c [string? {:arg-number 2}]]]\n```\n\n- Non-option arguments are supported directly amongst options, or after a\n  double-dash so `-a 1 ARG0 -b 2 -- ARG1 ARG2` may be equivalent to:\n\n``` clj\n{:a 1\n :b 2\n :piotr-yuxuan.malli-cli/arguments [\"ARG0\" \"ARG1\" \"ARG2\"]}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:a [boolean? {:arg-number 0}]]\n [:b string?]]\n```\n\n- Grouped short flags like `-hal` are expanded like, for example:\n\n``` clj\n{:help true\n :all true\n :list true}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:help [boolean? {:short-option \"-h\" :arg-number 0}]]\n [:all [boolean? {:short-option \"-a\" :arg-number 0}]]\n [:list [boolean? {:short-option \"-l\" :arg-number 0}]]]\n```\n\n- Non-idempotent options like `-vvv` are supported and may be rendered as:\n\n``` clj\n{:verbosity 3}\n;; or, depending on what you want:\n{:log-level :debug}\n\n;; Example schemas:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:log-level [:and\n              keyword?\n              [:enum {:short-option \"-v\"\n                      :short-option/arg-number 0\n                      :short-option/update-fn malli-cli/non-idempotent-option\n                      :default :error}\n               :off :fatal :error :warn :info :debug :trace :all]]]]\n\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:verbosity [int? {:short-option \"-v\"\n                    :short-option/arg-number 0\n                    :short-option/update-fn (fn [options {:keys [in]} _cli-args]\n                                              (update-in options in (fnil inc 0)))\n                    :default 0}]]]\n```\n\n- You may use nested maps in your config schema so that `--proxy-host\n  https://example.org/upload --proxy-port 3447` is expanded as:\n\n``` clj\n{:proxy {:host \"https://example.org/upload\"\n         :port 3447}}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:proxy [:map\n          [:host string?]\n          [:port pos-int?]]]]\n```\n\n- Namespaced keyword are allowed, albeit the command-line option name stays\n  simple `--upload-parallelism 32` may give:\n\n```clj\n{:upload/parallelism 32}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:upload/parallelism pos-int?]]\n```\n\n- You can provide your own code to update the result map with some complex\n  behaviour, like for example `--name Piotr`:\n\n``` clj\n{:vanity-name \"\u003e\u003e Piotr \u003c\u003c\"\n :original-name \"Piotr\"\n :first-letter \\P}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:vanity-name [string? {:long-option \"--name\"\n                         :update-fn (fn [options {:keys [in]} [username]]\n                                      (-\u003e options\n                                          (assoc :vanity-name (format \"\u003e\u003e %s \u003c\u003c\" username))\n                                          (assoc :original-name username)\n                                          (assoc :first-letter (first username))))}]]\n [:original-name string?]\n [:first-letter char?]]\n```\n\n- Build a simple summary string (see schema Config above):\n\n``` txt\n  -h  --help        nil\n  -a  --upload-api  \"http://localhost:8080\"  Address of target upload-api instance.\n  -v  --log-level   :error\n      --proxy-host  nil\n      --proxy-port  nil\n```\n\n- Error handling with unknown options:\n\n``` clojure\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:my-option string?]]\n\n;; Example input:\n[\"--unknown-long-option\" \"--other-option\" \"VALUE\" \"-s\"}\n\n;; Exemple output:\n#:piotr-yuxuan.malli-cli{:unknown-option-errors ({:arg \"-s\"} {:arg \"--other-option\"} {:arg \"--unknown-long-option\"}),\n                         :known-options (\"--my-option\"),\n                         :arguments [\"VALUE\"],\n                         :cli-args [\"--unknown-long-option\" \"--other-option\" \"VALUE\" \"-s\"]}\n```\n\n- Environment variable `USER` set to `piotr-yuxuan` may give:\n\n``` clojure\n{:user \"piotr-yuxuan\"}\n\n;; Example schema:\n[:map {:decode/args-transformer malli-cli/args-transformer}\n [:user [string? {:env-var \"USER\"}]]]\n```\n\n- Protect secret values and keep them out of logs when configuration is printed.\n  Secrets are turned into strings by default, but you may provide\n  custom `secret-fn` and `plaintext-fn`.\n\n``` clojure\n(m/encode\n  [:map\n   [:ui/service-name string?]\n   [:database/username {:secret true} string?]\n   [:database/password {:secret true} string?]]\n  {:ui/service-name \"Hello, world!\"\n   :database/username \"Username must stay out of logs.\"\n   :database/password \"Password must stay out of logs.\"}\n  malli-cli/secret-transformer)\n\n=\u003e {:ui/service-name \"Hello, world!\",\n    :database/username \"***\",\n    :database/password \"***\"}\n```\n\nNote that environment variables behave like default values with lower priority\nthan command-line arguments. Env vars are resolved at decode time, not at schema\ncompile time. This lack of purity is balanced by the environment being constant\nand set by the JVM at start-up time.\n\n# References\n\n- [GNU Program Argument Syntax Conventions](https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html)\n- [POSIX.1-2017 Utility argument syntax](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html)\n- https://github.com/l3nz/cli-matic is a similar, more established project.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpiotr-yuxuan%2Fmalli-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpiotr-yuxuan%2Fmalli-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpiotr-yuxuan%2Fmalli-cli/lists"}