Giter Site home page Giter Site logo

l3nz / cli-matic Goto Github PK

View Code? Open in Web Editor NEW
355.0 9.0 29.0 403 KB

Compact, hands-free [sub]command line parsing library for Clojure.

License: Eclipse Public License 2.0

Clojure 96.69% Shell 3.31%
clojure argv-parser command-line command-line-parser graalvm planck subcommands

cli-matic's Introduction

CLI-matic

Clojars Project ClojarsDownloads

All Contributors

Compact [sub]command line parsing library, for Clojure. Perfect for scripting (who said Clojure is not good for scripting?).

Especially when scripting, you should write interesting code, not boilerplate. Command line apps are usually so tiny that there is absolutely no reason why your code should not be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.

CLI-matic works with GraalVM, giving unbeatable performance for stand-alone command-line apps that do not even need a Java installation - see Command-line apps with Clojure and GraalVM: 300x better start-up times.

CLI-matic also works with Planck REPL for very quick CLJS scripting - see Using with Planck and - last but not least - is compatible with Babashka, that happens to be the gold standard of Clojure scripting.

And if there is no such thing as too much of a good thing, then BabashkaBins lets you write a script using bb and then compile it to a Graal binary automagically.

Using

The library is available on Clojars: https://clojars.org/cli-matic

Or the library can be easily referenced through Github when using deps (make sure you change the commit-id):

{:deps
 {cli-matic
  {:git/url "https://github.com/l3nz/cli-matic.git"
   :sha "374b2ad71843c07b9d2ddfc1d4439bd7f8ebafab"}}}

Features

  • Create all-in-one scripts with subcommands and help, in a way more compact than the excellent - but lower level - tools.cli.
  • Avoid common pre-processing. Parsing dates, integers, reading small files, downloading a JSON URL.... it should just happen. The more you declare, the less time you waste.
  • Validate with Spec. Modern Clojure uses Spec, so validation should be spec-based as well. Validation should happen at the parameter level, and across all parameters of the subcommand at once, and emit sane error messages. Again, the more you have in declarative code, the less room for mistakes.
  • Read environment variables. Passing environment variables is a handy way to inject passwords, etc. This should just happen and be declarative.
  • Capture unnamed parameters as if they were named parameters, with casting, validation, etc.
  • Babashka-compatible. Read here for more info.

While targeted at scripting, CLI-matic of course works with any program receiving CLI arguments.

Rationale

Say we want to create a short script, in Clojure, where we want to run a very simple calculator that either sums A to B or subtracts B from A:

$ clj -m calc add -a 40 -b 2
42
$ clj -m calc sub -a 10 -b 2
8
$ clj -m calc --base 16 add -a 30 -b 2
20

We also want it to display its help:

$clj -m calc -?
NAME:
 toycalc - A command-line toy calculator

USAGE:
 toycalc [global-options] command [command options] [arguments...]

VERSION:
 0.0.1

COMMANDS:
   add, a   Adds two numbers together
   sub, s   Subtracts parameter B from A

GLOBAL OPTIONS:
       --base N  10  The number base for output
   -?, --help

And help for sub-commands:

$clj -m calc add -?
NAME:
 toycalc add - Adds two numbers together

USAGE:
 toycalc [add|a] [command options] [arguments...]

OPTIONS:
   -a, --a1 N  0  Addendum 1
   -b, --a2 N  0  Addendum 2
   -?, --help

But while we are coding this, we do not really want to waste time writing any parsing logic. What we care about implementing are the functions add-numbers and sub-numbers where we do actual work; the rest should be declared externally and/or "just happen".

From the point of view of us programmers, we'd like to have a couple of functions like:

(defn add-number
	"Sums A and B together, and prints it in base `base`"
	[{:keys [a b base]}]
	(Integer/toString (+ a b) base))

And nothing more; the fact that both parameters exist, are of the right type, have the right defaults, print the correct help screen, etc., should ideally not be a concern.

So we define a configuration:

(def CONFIGURATION
  {:command     "toycalc"
   :description "A command-line toy calculator"
   :version     "0.0.1"
   :opts        [{:as      "The number base for output"
                  :default 10
                  :option  "base"
                  :type    :int}]
   :subcommands [{:command     "add"
                  :description "Adds two numbers together"
                  :examples    ["First example" "Second example"]
                  :opts        [{:as     "Addendum 1"
                                 :option "a"
                                 :type   :int}
                                {:as      "Addendum 2"
                                 :default 0
                                 :option  "b"
                                 :type    :int}]
                  :runs        add_numbers}
                 {:command     "subc"
                  :description "Subtracts parameter B from A"
                  :opts        [{:as      "Parameter q"
                                 :default 0
                                 :option  "q"
                                 :type    :int}]
                  :subcommands [{:command     "sub"
                                 :description "Subtracts"
                                 :opts        [{:as      "Parameter A"
                                                :default 0
                                                :option  "a"
                                                :type    :int}
                                               {:as      "Parameter B"
                                                :default 0
                                                :option  "b"
                                                :type    :int}]
                                 :runs        subtract_numbers}]}]} ]

It contains:

  • Information on the app itself (name, version)
  • The list of global parameters as :opts, i.e. the ones that apply to all subcommands (may be empty, or you may skip it at all)
  • A list of sub-commands, each with its own parameters in :opts, and a function to be called in :runs, or more :subcommands. You can optionally validate the full parameter-map that is received by the function implementing the subcommand at once by passing a Spec into :spec.

And...that's it!

Handling multiple layers of sub-commands

As the configuration is recursive (what you have in :subcommands can contain more subcommands) you can have multiple layers of subcommands, each with their own "global" options; or you can have no subbcommands at all by simply defining a :runs function at the main level.

  • If within the subcommand you add a 0-arity function to :on-shutdown, it will be called when the JVM terminates. This is mostly useful for long running servers, or to do some clean-up. Note that the hook is always called - whether the shutdown is forced by pressing (say) Ctrl+C or just by the JVM exiting. See the examples.
  • When printing a version number, the most-specific wins; that is, you could have a different version string per subcommand. If not found, the most-specific ancestor found is used.
  • The same goes for help generation; you can have it customized per sub-command if needed.
  • Each subcommand can have an optional :examples key, that can contain a string or a sequence of strings, that will be printed out under EXAMPLES.

Current pre-sets

The following pre-sets (:type) are available:

  • :int - an integer number
  • :int-0 - an integer number, with defaults to zero
  • :float - a float number
  • :float-0 - a float number, with defaults to zero
  • :string - a string
  • :keyword - a string representation of a keyword, leading colon is optional, if no namespace is specified. ::foo will be converted to :user/foo, otherwise it will work as expected.
  • :with-flag - a boolean flag that generates a pair of --foo/--no-foo flags. --foo sets 'foo' to true and --no-foo sets 'foo' to false.
  • :flag - a boolean flag that recognizes "Y", "Yes", "On", "T", "True", and "1" as true values and "N", "No", "Off", "F", "False", and "0" as false values.
  • :json - a JSON literal value, that will be decoded and returned as a Clojure structure.
  • :yaml - a YAML literal value, that will be decoded and returned as a Clojure structure.
  • :edn - an EDN literal value, that will be decoded and returned.
  • :yyyy-mm-dd - a Date object, expressed as "yyyy-mm-dd" in the local time zone
  • :slurp - Receives a file name - reads is as text and returns it as a single string. Handles URIs correctly. Handles "-" as stdin.
  • :slurplines - Receives a file name - reads is as text and returns it as a seq of strings. Handles URIs correctly.
  • :ednfile - a file (or URL) containing EDN, that will be decoded and returned as a Clojure structure.
  • :jsonfile - a file (or URL) containing JSON, that will be decoded and returned as a Clojure structure.
  • :yamlfile - a file (or URL) containing YAML, that will be decoded and returned as a Clojure structure.

You may also specify a set of allowed values in :type, like :type #{:one :two}. It must be a set made of keywords or strings, and the parameter will be matched to allowed values in a case-insensitive way. Keywords do not need (but are allowed) a trailing colon. Sets print their allowed values on help and, on mismatches, suggest possible correct values.

For all options, you can then add:

  • :default the default value, as expected after conversion
    • If no default, the value will be passed only if present
    • If you set :default :present this means that CLI-matic will abort if that option is not present; it is effectively a “required option” (and it appears with a trailing asterisk in the help)
  • :as is the description that appears in help. It can be a multi-line array, or a single string.
  • :multiple if true, the values for all options with the same name are stored in an array
  • :short: a shortened name for the command (if a string), or a positional argument if integer (see below).
  • :env if set, the default is read from the current value of an env variable you specify. For capture to happen, either the option must be missing, or its value must be invalid. If an option has an :env value specified to FOO, its description in the help shows [$FOO].
  • :spec: a Spec that will be used to validate the the parameter, after any coercion/transformation.

Return values

The function called can return an integer; if it does, it is used as an exit code for the shell process.

If you return a future, or a promise, or a core.async channel, then CLI-matic will wait until it is fulfilled, or there is a value on the channel, and will use that as a return code (at the moment, only works on the JVM).

Errors and exceptions return an exit code of -1; while normal executions (including invocations of help) return 0.

Positional arguments

If there are values that are not options in your command line, CLI-matic will usually return them in an array of unparsed entries, as strings. But - if you use the positional syntax for short:

{:option "a1" :short 0 :as "First addendum" :type :int :default 23}

You 'bind' the option 'a1' to the first unparsed element; this means that you can apply all presets/defaults/validation rules as if it was a named option.

So you could call your script as:

clj -m calc add --a2 3 5

And CLI-matic would set 'a2' to 3 and have "5" as an unparsed argument; and then bind it to "a1", so it will be cast to an integer. You function will be called with:

{:a1 5, :a2 3}

That is what you wanted from the start.

At the same time, the named option remains, so you can use either version. Bound entries are not removed from the unparsed command line entries.

Validation with Spec (and Expound)

CLI-matic can optionally validate any parameter, and the set of parameters you use to call the subcommand function, with Spec, and uses the excellent Expound https://github.com/bhb/expound to produce sane error messages. An example is under examples/clj as toycalc-spec.clj - see https://github.com/l3nz/cli-matic/blob/master/examples/clj/toycalc-spec.clj

By using and including Expound as a depencency, you can add error descriptions where the raw Spec would be hard to read, and use a nice set of pre-built specs with readable descriptions that come with Expound - see https://github.com/bhb/expound/blob/master/src/expound/specs.cljc

Help text generation

CLI-matic comes with pre-packaged help text generators for global and sub-command help. These generators can be overridden by supplying one or more of your own functions in the :app section of the configuration:

(defn my-command-help [setup subcmd]
  " ... ")

(defn gen-sub-command-help [setup subcmd]
  " ... ")

{:command "toycalc"
 :global-help my-command-help
 :subcmd-help gen-sub-command-help}}

Both functions receive the the configuration and the sub-command it was called with, and return a string (or an array of strings) that CLI-matic prints verbatim to the user as the full help text.

See example in helpgen.clj.

Babashka

This library is compatible with Babashka - a native Clojure interpreter for scripting with fast startup. Its main goal is to leverage Clojure in places where you would be using bash otherwise.

In addition to this library, you need to include babashka's fork of clojure.spec.alpha in your bb.edn. Also see this project's bb.edn for how this project's tests are run with babashka.

See Scripting with Babashka.

Old (non-recursive) configuration

The following configuration, that forced you to use exactly one layer, is still supported and translated automagically.

(def CONFIGURATION
  {:app         {:command     "toycalc"
                 :description "A command-line toy calculator"
                 :version     "0.0.1"}

   :global-opts [{:option  "base"
                  :as      "The number base for output"
                  :type    :int
                  :default 10}]

   :commands    [{:command     "add"
                  :description "Adds two numbers together"
                  :opts        [{:option "a" :as "Addendum 1" :type :int}
                                {:option "b" :as "Addendum 2" :type :int :default 0}]              
                  :runs        add_numbers}

                 {:command     "sub"
                  :description "Subtracts parameter B from A"
                  :opts        [{:option "a" :as "Parameter A" :type :int :default 0}
                                {:option "b" :as "Parameter B" :type :int :default 0}]
                  :runs        subtract_numbers}
                 ]})

Note that custom help-text generators are not translated, as their arity changed in v0.4.0+

Transitive dependencies

CLI-matic currently depends on:

  • org.clojure/clojure
  • org.clojure/spec.alpha
  • org.clojure/tools.cli
  • expound

Optional dependencies

To use JSON decoding, you need Cheshire cheshire/cheshire to be on the classpath; otherwise it will break. If you do not need JSON parsing, you can do without.

To use Yaml decoding, you need clj-commons/clj-yaml on your classpath; otherwise it will break. If you do not need YAML parsing, you can do without. Note that up to version 0.4 of cli-matic we used to rely on io.forward/yaml, but it used reflection, and so was incompatible with GraalVM native images.

If Orchestra orchestra is present on the classpath, loading most namespaces triggers an instrumentation. As we already have Expound, we get easy-to-read messages for free.

Tips & tricks

Reducing startup time with skip-macros

If you run your script with the property clojure.spec.skip-macros=true you get significant savings:

	time clj -J-Dclojure.spec.skip-macros=true -m recap sv
	real	0m2.587s - user	0m6.997 - sys	0m0.332s

Versus the default:

	time clj -J-Dclojure.spec.skip-macros=false -m recap sv
	real	0m3.141s - user	0m8.707s - sys	0m0.391s

So that's like half a second for free on my machine.

Capturing current version

If you would like to capture the build environment at compile time (e.g. the exact GIT revision, or when/where the program was built, or the version of your project as defined in project.clj) so you can print meaningful version numbers without manual intervention, you may want to include https://github.com/l3nz/say-cheez and use it to provide everything to you.

Writing a stand-alone script with no external deps.edn

Eric Normand has a nice tip for writing stand-alone scripts that all live in one file:

#!/bin/sh
#_(
   #_DEPS is same format as deps.edn. Multiline is okay.
   DEPS='
   {:deps 
   	{cli-matic {:mvn/version "0.3.3"}}}
   '

   #_You can put other options here
   OPTS='
   -J-Xms256m -J-Xmx256m 
   -J-client
   -J-Dclojure.spec.skip-macros=true
   '
exec clojure $OPTS -Sdeps "$DEPS" "$0" "$@"
)

(println "It works!")

And so you have a nice place not to forget to set skip-macros!

BabashkaBins

bbb lets you take a standard Clojure project layout, run it under both JVM Clojure and babashka, and then automates the compilation of your project into a static binary with GraalVM for you when it’s time to distribute it.

Contributing

Before submitting a bug or pull request, make sure you read CONTRIBUTING.md.

Similar projects / inspiration

License

The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found in the file epl.html at the root of this distribution. By using this software in any fashion, you are agreeing to be bound by the terms of this license.

You must not remove this notice, or any other, from this software.

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Jason Whitlark

💻

ty-i3

💻

Ivan Kuznetsov

💻

Clemens Damke

💻

Burin Choomnuan

💻

Lee Read

💻

Mike Fikes

💬

This project follows the all-contributors specification. Contributions of any kind welcome!

cli-matic's People

Contributors

agilecreativity avatar allcontributors[bot] avatar borkdude avatar cortys avatar ieugen avatar jeiwan avatar jwhitlark avatar l3nz avatar lread avatar noahtheduke avatar the-alchemist avatar timokramer avatar ty-i3 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cli-matic's Issues

Feature Request: Ability to override generation help text

This feature request adds optional functions that override the way cli-matic prints help, affording complete byte-by-byte control over the help text.

Rationale: Our internal CLI tooling needs to adhere to certain policies regarding help and usability for developers. Certain info such as Git build, version string, and grouping of commands needs to be printed in the prescribed format. This feature request would realize the ability to completely control the presentation of generated help text to satisfy those policies.

An example of generating global help:

(def banner
  (str "Welcome to CLI-ABC123, Git "
       build-info/git-hash))

(defn commands-segment
  [cfg]
  (str "Commands:\n"
       (clojure.string/join
         "\n"
         (map
           #(format "   %-10s %s"
                    (get % :command)
                    (get % :description))
           (cfg :commands)))))

(defn my-global-help
  [cfg]
  (clojure.string/join "\n" [banner (commands-segment cfg) ...]))

and using it as the global help text generator:

(run-cmd args {:app {:global-help my-global-help
                     ...}})

Lots of useless output in 0.1.16

"SUB: globals" [{:option "base", :as "The number base for output", :type :int, :default 10}]
"SUB: def" [{:option "a1", :short "a", :env "AA", :as "First addendum", :type :int, :default 0} {:option "a2", :short "b", :as "Second addendum", :type :int, :default 0}]
"Spec for subcmd " #object[clojure.core$constantly$fn__5394 0x79c3f01f "clojure.core$constantly$fn__5394@79c3f01f"]
"Validating Specs" [{:option "base", :as "The number base for output", :type :int, :default 10}] {:base 16, :a1 1, :a2 254, :_arguments []}
"Validating Specs" [{:option "a1", :short "a", :env "AA", :as "First addendum", :type :int, :default 0} {:option "a2", :short "b", :as "Second addendum", :type :int, :default 0}] {:base 16, :a1 1, :a2 254, :_arguments []}
"Failing global" nil
"Failing local" nil
"Failing total" nil

We don't want that.

Slurping of whole file at once

We should have an option to slurp a whole file at once given a filename, and return it as big string, vector of strings or decoded-json (with optional deps on cheshire).

Using :default for mandatory options

We have three cases:

  • a totally optional option, no default - if there is it appears in opts, if not it's not there - you just don't specify a :default
  • a totally optional option, with default - it always appears in option map (worst case with default value) - e.g. :default 3
  • a mandatory option without default - any value is good but you must set it - we could handle it by setting :default :present

In the third case, we will abort with an error message if item is not present in after-parse option map.

Missing global options do not cause errors if you call a subcommand only for getting its help page.

Use separate namespaces

It would make sense to have separate namespaces for:

  • general utils
  • code that generates help text (that is rather big)

This way we could have a smaller core.

It is just refactoring and tests.

Can't change/establish root binding of: *explain-out*

Caused by: java.lang.IllegalStateException: Can't change/establish root binding of: explain-out with set
at clojure.lang.Var.set(Var.java:223)
at cli_matic.optionals$orchestra_instrument.invokeStatic(optionals.clj:70)
at cli_matic.optionals$orchestra_instrument.invoke(optionals.clj:56)
at cli_matic.presets__init.load(Unknown Source)
at cli_matic.presets__init.(Unknown Source)

Support for STDIN

I hacked something together that looks like

 (let
   [bf (java.io.BufferedReader. *in*)]
    (if
     (.ready bf)
      (let
       [conf (slurp *in*) vargs (into [] args)]
        (run-cmd (conj vargs "--stdin" conf) STDIN_INPUT))
      (run-cmd args FILE_INPUT))))

where STDIN_INPUT

(def
  STDIN_INPUT
  {:app
   {:command "nparser",
    :description "A command-line configuration generator",
    :version "0.1.2"},
   :global-opts [],
   :commands
   [{:command "to-json",
     :description "Generate JSON from a config",
     :opts
     [{:option "stdin",
       :as "Config input file",
       :type :string,
       :default :present}
      {:option "grammar",
       :as "Grammar file",
       :type :string,
       :default :present}],
     :runs gen-json}
    {:command "to-config",
     :description "Generate config from an input file",
     :opts
     [{:option "stdin",
       :as "JSON input file",
       :type :string,
       :default :present}],
     :runs gen-config}]})

not very idiomatic, but would be great to incorporate this into cli-matic..

The primary use-case is now that we have GRAALVM we can native-compile CLJ code into binary artifacts to build command-line apps..

Multiple aliases for commands

We could have multiple aliased for each subcommand, like e.g.

COMMANDS:
 add, a     adds 
 sub, s     subtracts

So writing

mycmd add --a 1 --b 2

and

mycmd a --a 1 --b 2

are the very same thing.

Error with new

Exception in thread "main" clojure.lang.ExceptionInfo: Call to #'cli-matic.help-gen/generate-help-possible-mistypes did not conform to spec:
core.cljc:364

-- Spec failed --------------------

Return value

  [... ... ... [" vendite"]]
               ^^^^^^^^^^^^

should satisfy

  string?

But it's actually a vector of strings OR vectors of strings. So it works but spec is wrong.

Regression: Spec fails on 0.1.13

This is because we call rewrite-opts with three parameters in get-options-summary.
Fix is easy, but how can we know it will not happen again?

Planck: Remove exceptions

These should not be there:

WARNING: Use of undeclared Var cli-matic.core/Throwable at line 74 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/Throwable at line 233 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/IllegalAccessException at line 422 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/IllegalAccessException at line 422 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/IllegalAccessException at line 454 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/IllegalAccessException at line 454 cli_matic/core.cljc
WARNING: Use of undeclared Var cli-matic.core/Throwable at line 524 cli_matic/core.cljc

Add shutdown hook

If the command run is long-running, it would be nice to add a shutdown hook by calling :on-shutdown:

(.addShutdownHook
            (Runtime/getRuntime)
            (Thread.
              (fn []
                (log "Bye!")
                )))

Interactive mode - why not?

If we have:

OPTIONS:
   -a, --a1 N  0  First addendum [$AA]
   -b, --a2 N  0  Second addendum
   -?, --help

Why shouldn't we have:

$ toycalc add --interactive

First addendum (a1, def 0): 10
Second addendum (a2, def 0): 20

You are about to add 10 and 20 together. Are you sure you want to continue? Y
30

Positional parameters

Why don't we use the same mechanism we use for options to pull positional argument?

Like - instead of:

{:option "pa" :short "a" :as "Parameter A" :type :int :default 0}

we could have

{:option "pa" :short 0 :as "Parameter A" :type :int :default 0}

This would use the first free parameter, store it in "pa" and check it's an int.

Pulled positional arguments remain anyway available in the arguments vector.

Positional arguments are only valid on sub-commands and must appear on the help line.

Multiline descriptions for options.

Thanks for this library @l3nz, it's really quite fantastic how easy and concise it makes all this stuff, I added an improved CLI interface to cljdoc using it: cljdoc/cljdoc@1ce56b8

For some subcommands I'd like to specify longer descriptions, spanning a couple of lines. I was wondering if there's any tooling or plans around this?

I could construct a :description string with linebreaks myself but I think cli-matic could help with the following aspects:

  • trim leading whitespace in some smart way (maybe codox' approach is sufficient)
  • automatically break lines longer than 80 characters
  • maintain the one-character indentation under NAME:

FWIW there could also be a new key :usage for longer text, reserving :description for a one-line overview.

Unrelated feedback: it took me a while to spot the :default :present variant in the README when trying to figure out how to make options mandatory. Maybe introducing another key :required? or so would make this easier.

Boolean (flag) data types

I see a few boolean options:

  • flags like --flag (true) or --no-flag (false). The first one could be shortened to -f
  • flags with parameters, like --flag=1. Would expect to recognise "Y", "Yes", "On", "T", "True" and "1" as truthy. Falsey would be "N", "No", "Off", "F", "False" and "0". Other values would raise an error.

Add EDN support

Wouldn't be optional, as it's included with Clojure. I'm working on it.

Example with GraalVM

I think we should have an example/tutorial of how to make a simple Cli app that runs within GraalVM. I am sure it would have a decent success and in the end it's what everybody would love - simple CLI commands that are small, portable and start up in no time.

Add :multiple tag for options

If an option is tagged as :multiple true, then it is actually a vector of all parameter invocations.

     cmd sub --a 1 --a 2 --a 3

where :a is tagged as :multiple, should get you

      {:a [1 2 3]}

In the order they are passed.

Read values from environment variables

If an option is tagged as ':env ABCD'; it will

  • load the default from ABCD (but it can be overridden through the command line)
  • display "ABCD" in the help for that option

Do not abort on wrong return type.

If we return anything from our main method that is not a number, it crashes!

JVM Exception: #error {
 :cause clojure.lang.PersistentArrayMap cannot be cast to java.lang.Number
 :via
 [{:type java.lang.ClassCastException
   :message clojure.lang.PersistentArrayMap cannot be cast to java.lang.Number
   :at [clojure.lang.Numbers isZero Numbers.java 92]}]
 :trace
 [[clojure.lang.Numbers isZero Numbers.java 92]
  [cli_matic.core$invoke_subcmd invokeStatic core.clj 412]
  [cli_matic.core$invoke_subcmd invoke core.clj 405]
  [cli_matic.core$run_cmd_STAR_ invokeStatic core.clj 442]
  [cli_matic.core$run_cmd_STAR_ invoke core.clj 428]
  [cli_matic.core$run_cmd invokeStatic core.clj 447]
  [cli_matic.core$run_cmd invoke core.clj 444]

We should just ignore them and return zero.

Have double :short option.

We should get a better message out.

Exception in thread "main" java.lang.AssertionError: Assert failed: (distinct?* (remove nil? (map :short-opt %)))
	at clojure.tools.cli$compile_option_specs.invokeStatic(cli.clj:265)
	at clojure.tools.cli$compile_option_specs.invoke(cli.clj:265)
	at clojure.tools.cli$parse_opts.invokeStatic(cli.clj:593)
	at clojure.tools.cli$parse_opts.doInvoke(cli.clj:471)
	at clojure.lang.RestFn.invoke(RestFn.java:425)
	at cli_matic.core$parse_cmds.invokeStatic(core.clj:417)
	at cli_matic.core$parse_cmds.invoke(core.clj:372)
	at clojure.lang.AFn.applyToHelper(AFn.java:156)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at orchestra.spec.test$spec_checking_fn$fn__727.doInvoke(test.clj:123)
	at clojure.lang.RestFn.invoke(RestFn.java:421)
	at cli_matic.core$run_cmd_STAR_.invokeStatic(core.clj:521)
	at cli_matic.core$run_cmd_STAR_.invoke(core.clj:519)
	at cli_matic.core$run_cmd.invokeStatic(core.clj:543)
	at cli_matic.core$run_cmd.invoke(core.clj:535)

Use tools.cli v 0.4.1

Not sure whether there are any changes that affect us, but it's better to use the current version in any case.

Better startup times: skip macros

It would be worthwhile too add some tips section for better startup times, e.g.

time clj -J-Dclojure.spec.skip-macros=true -m recap sv

real	0m2.587s - user	0m6.997 - sys	0m0.332s

versus default:

time clj -J-Dclojure.spec.skip-macros=false -m recap sv

real	0m3.141s -user	0m8.707s -sys	0m0.391s


Writing it here so I don't forget 😸

Don't instrument functions by default

cli-matic.core calls orchestra.spec.test/instrument when being loaded. This instruments all spec'ed functions and not just cli-matic's own API. In my case this brought up surprising issues where I didn't expect them.

Would you consider a PR where only cli-matic's own functions are instrumented?

Add YAML support (optional like json)

I do a lot of work with Kubernetes stuff, and YAML is the preferred format.

I'm working on this in my fork; I'll open a pull request when it's ready.

Command validation using spec

We should be able to validate all options to a command using spec, with a better error message than the default one.

Set data types

Sometimes you have a given set of string values that are the only available options.

We want to:

  • display the allowed set on help
  • cast the string to a keyword
OPTIONS:
   -k, --kind S*         Kind of mailing: (:Q :Q-REN :Q-UPG)

JSON parameters

Why not having

    --data '{"a": "b"}'

that returns {:data {"a" "b"}} as a parameter, as long as you have Cheshire on the classpath?

And why you are at it, why not having

    --data @somedata.json

That returns the contents of somedata.json as a parsed structure?

Short names for options (aliases)

We want to have shorter and optional names for both options and subcommands.

The names should appear in the online help and of course be used at all.

Multiline description

We could have an optional multi-line description added to the utility and to each subcommand, to be displayed in the help screens.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.