All Projects → piotr-yuxuan → malli-cli

piotr-yuxuan / malli-cli

Licence: EUPL-1.2 License
Command-line interface from the comfort of a metosin/malli schema

Programming Languages

clojure
4091 projects

Projects that are alternatives of or similar to malli-cli

Speed Test
Test your internet connection speed and ping using speedtest.net from the CLI
Stars: ✭ 3,654 (+10050%)
Mutual labels:  cli-app, command-line-tool
Pyinquirer
A Python module for common interactive command line user interfaces
Stars: ✭ 1,151 (+3097.22%)
Mutual labels:  cli-app, command-line-tool
Mongo Seeding
The ultimate solution for populating your MongoDB database.
Stars: ✭ 375 (+941.67%)
Mutual labels:  cli-app, command-line-tool
Ttyplot
a realtime plotting utility for terminal/console with data input from stdin
Stars: ✭ 532 (+1377.78%)
Mutual labels:  cli-app, command-line-tool
Git Tidy
Tidy up stale git branches.
Stars: ✭ 137 (+280.56%)
Mutual labels:  cli-app, command-line-tool
doing-cli
CLI tool to simplify the development workflow on azure devops
Stars: ✭ 19 (-47.22%)
Mutual labels:  cli-app, command-line-tool
Initior
A command line application that let's you initialize your new projects the right way, replaces npm and yarn's init 🎆
Stars: ✭ 17 (-52.78%)
Mutual labels:  cli-app, command-line-tool
jira-cli
🔥 [WIP] Feature-rich interactive Jira command line.
Stars: ✭ 809 (+2147.22%)
Mutual labels:  cli-app, command-line-tool
Replace In Files Cli
Replace matching strings and regexes in files
Stars: ✭ 129 (+258.33%)
Mutual labels:  cli-app, command-line-tool
Terminal layout
The project help you to quickly build layouts in terminal,cross-platform(一个跨平台的命令行ui布局工具)
Stars: ✭ 98 (+172.22%)
Mutual labels:  cli-app, command-line-tool
Forge Node App
🛠📦🎉 Generate Node.js boilerplate with optional libraries & tools
Stars: ✭ 90 (+150%)
Mutual labels:  cli-app, command-line-tool
Geek Life
The Todo List / Task Manager for Geeks in command line
Stars: ✭ 212 (+488.89%)
Mutual labels:  cli-app, command-line-tool
Fast Cli
Test your download and upload speed using fast.com
Stars: ✭ 2,178 (+5950%)
Mutual labels:  cli-app, command-line-tool
wifiqr
Create a QR code with your Wi-Fi login details
Stars: ✭ 207 (+475%)
Mutual labels:  cli-app, command-line-tool
gst
👻 Supercharge your ghq workflow.
Stars: ✭ 34 (-5.56%)
Mutual labels:  cli-app
google-CommandLine-Translation-Tool
命令行翻译工具-谷歌-中英文
Stars: ✭ 22 (-38.89%)
Mutual labels:  command-line-tool
landscaper
Apply code mods to projects, awesomely
Stars: ✭ 15 (-58.33%)
Mutual labels:  command-line-tool
przm
🎨 A simple, yet feature rich color picker and manipulator
Stars: ✭ 19 (-47.22%)
Mutual labels:  command-line-tool
zswap-cli
Command-line tool to control zswap Linux kernel module options
Stars: ✭ 20 (-44.44%)
Mutual labels:  command-line-tool
terminal-style
🎨 Return your terminal message in style! Change the text style, text color and text background color from the terminal, console or shell interface with ANSI color codes. Support for Laravel and Composer.
Stars: ✭ 16 (-55.56%)
Mutual labels:  command-line-tool

piotr-yuxuan/malli-cli

Command-line interface with malli.

Build status Clojars badge Clojars downloads cljdoc badge GitHub license GitHub issues

What it offers

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:

  1. Default configuration value;
  2. Environment variables when the program starts up;
  3. Value from some configuration management system;
  4. Command line arguments.

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.

Maturity and evolution

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.

Naming

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".

Simple example

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? {:description "Print actual configuration value and exit."
                               :optional true
                               :arg-number 0}]]
     [:help [boolean? {:description "Display usage summary and exit."
                       :short-option "-h"
                       :optional true
                       :arg-number 0}]]
     [:upload-api [string? {:description "Address of target upload-api instance. If not set from the command line, lookup env var $CORP_UPLOAD_API."
                            :short-option "-a"
                            ;; Cli option will be looked up, then env var, then default.
                            :env-var "CORP_UPLOAD_API"
                            :default "http://localhost:8080"}]]
     [:log-level [:enum {:description "Non-idempotent -v increases log level, --log-level sets it."
                         ;; Express how to decode a string into an enum value.
                         :decode/string keyword
                         :short-option "-v"
                         :short-option/arg-number 0
                         :short-option/update-fn malli-cli/non-idempotent-option
                         :default :error
                         ;; Used in summary to pretty-print the default value to a string.
                         :default->str name}
                  :off :fatal :error :warn :info :debug :trace :all]]
     [:proxy [:map
              [:host string?]
              ;; malli will parse and return an integer.
              [:port pos-int?]]]]))

Here is the command summary produced by (malli-cli/summary Config) for this config:

  Short  Long option    Default                  Description
         --show-config                           Print actual configuration value and exit.
  -h     --help                                  Display usage summary and exit.
  -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.
  -v     --log-level    error                    Non-idempotent -v increases log level, --log-level sets it.
         --proxy-host
         --proxy-port

Let's try to call this program (code details below). You may invoke your Clojure main function with:

lein run \
  --help -vvv \
  -a "https://localhost:4000"

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:7000. Then the resulting configuration passed to your app will be:

{:help true
 :upload-api "https://localhost:4000"
 :log-level :debug
 :proxy {;; Nested config maps are supported
         :host "http://proxy.example.com"
         ;; malli transform strings into appropriate types
         :port 3128}

Let's try another time with this command with same provided config and env vars:

lein run \
  --log-level=off
  --show-config

The program will exit after printing:

{:log-level :off,
 :show-config? true,
 :upload-api "http://localhost:7000"}

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 '[clojure.pprint])
(require '[piotr-yuxuan.malli-cli.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 (clojure.pprint/pprint config)
              (System/exit 0))

          (:help config)
          (do (println (simple-summary Config))
              (System/exit 0))

          :else
          (app/start config))))

Capabilities

See tests for minimal working code for each of these examples.

  • Long option flag and value --long-option VALUE may give
{:long-option "VALUE"}

;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
  [:long-option string?]]
  • Grouped flag and value with --long-option=VALUE may give
{:long-option "VALUE"}

;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
  [:long-option string?]]
  • Short option names with -s VALUE may give
{:some-option "VALUE"}

;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
 [:some-option [string? {:short-option "-s"}]]]
  • Options that accept a variable number of arguments: -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}]]]
  • Non-option arguments are supported directly amongst options, or after a double-dash so -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?]]
  • Grouped short flags like -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}]]]
  • Non-idempotent options like -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}]]]
  • You may use nested maps in your config schema so that --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?]]]]
  • Namespaced keyword are allowed, albeit the command-line option name stays simple --upload-parallelism 32 may give:
{:upload/parallelism 32}

;; Example schema:
[:map {:decode/args-transformer malli-cli/args-transformer}
 [:upload/parallelism pos-int?]]
  • You can provide your own code to update the result map with some complex behaviour, like for example --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?]]
  • Build a simple summary string (see schema Config above):
  -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
  • Error handling with unknown options:
;; 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"]}
  • Environment variable 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.

References

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].