net.lewisship.multi
is a complement to joker.tools.cli that allows a single tool, a Joker script, tocontain multiple commands, each with its own command line options and arguments.
At the core is thenet.lewisship.multi/dispatch
macro; this identifies the name of the tool, andoptionally, the namespaces to collect commands from.
Each command within the tool is provided via thenet.lewisship.multi/defcommand
macro.
A command option,-h / --help
, is added to all commands automatically.
Ahelp
command is also added; it displays the list of commands available.The help command displays the first line of each command's docstring, as a summaryof the command.
Thehelp
command displays a short summary of what the overall tool does; this is the docstringof the first namespace provided todispatch
(again, if omitted, the current namespaceis used).
Whereas command option parsing is driven by the option names, command argumentparsing is positional. Each command option spec will consume one command line argument(exception: the last spec may be repeatable).
command argument specs are similar to command option specs; each spec is a vector that startswith one or two strings; the first string is always the label (by convention,in upper case). The optional second string is the documentation of the argument.
Following that are key/value pairs:
:id
(keyword) - identifies which key is used in the arguments map; by default,this is the label converted to a lower case keyword
:doc
(string) - documentation for the argument
:optional
(boolean) -- if true, the argument may be omitted if there isn't acommand line argument to match
:repeatable
(boolean) -- if true, then any remaining command line arguments are processedby the argument
:parse-fn
- passed the command line argument, returns a value, or throws an exception
:validate
- a vector of function/message pairs
:update-fn
- optional function used to update the (initially nil) entry for the argument in the :arguments map
:assoc-fn
- optional function used to update the arguments map; passed the map, the id, and the parsed value
:update-fn
and:assoc-fn
are mutually exclusive.
For repeatable arguments, the default update function will construct a vector of values.For non-repeatable arguments, the default update function simply sets the value.
#!/usr/bin/env joker(ns-sources {"net.lewisship.multi" {:url "https://raw.githubusercontent.com/hlship/multi/v1.4.0/src/net/lewisship/multi.joke"}})(ns example "System management tool" (:require [net.lewisship.multi :as multi]))(multi/defcommand configure "Configures the system with keys and values" [verbose ["-v" "--verbose" "Enable verbose logging"] :args host ["HOST" "System configuration URL" :validate [#(re-matches #"https?://.+" %) "must be a URL"]] key-values ["DATA" "Data to configure as KEY=VALUE" :parse-fn (fn [s] (when-let [[_ k v] (re-matches #"(.+)=(.+)" s)] [(keyword k) v])) :update-fn (fn [m [k v]] (assoc m k v)) :repeatable true]] (prn :verbose verbose :host host :key-values key-values);; Execution:(multi/dispatch {:tool-name "example"]})
If this file is saved asbin/example
, it can be executed directly:
> bin/example configure --helpUsage: example configure [OPTIONS] HOST DATA+Configures the system with keys and valuesOptions: -v, --verbose Enable verbose logging -h, --help This command summaryArguments: HOST: System configuration URL DATA: Data to configure as KEY=VALUE> bin/example configure http://localhost:9983 retries=3 alerts=on -v:verbose true :host "http://localhost:9983" :key-values {:retries "3", :alerts "on"}
Notes:
- With
defcommand
a docstring is required - After the docstring comes theinterface, which defines options and arguments
- The interface initially expects to consume a symbol then an option spec
- The
:args
keyword switches over to consuming symbol and argument spec
Thedispatch
macro normally searches fordefcommand
s in the same namespace, but this can beoverridden by providing a list of namespace symbols as the:namespaces
option.
Tool documentation comes from the docstring of the first namespace.
Inside the interface, you can request thecommand map using:as
.This command map is used when invokingnet.lewisship.multi/show-summary
,which a command may wish to do to present errors to the user.
You may also override the command name away from its default (the name of the function).
For example, if you had an existingconfigure
function you didn't want to rename,then you could name the command functionconfigure-command
:
(multi/defcommand configure-command "Configures the system with keys and values" [verbose ["-v" "--verbose" "Enable verbose logging"] :args host ["HOST" "System configuration URL" :validate [#(re-matches #"https?://.+" %) "must be a URL"]] key-values ["DATA" "Data to configure as KEY=VALUE" :parse-fn (fn [s] (when-let [[_ k v] (re-matches #"(.+)=(.+)" s)] [(keyword k) v])) :update-fn (fn [m [k v]] (assoc m k v)) :repeatable true] :command-name "configure"] ...)
Finally, you can specify additional meta data for the command by applyingit to the command's symbol. For example:
(multi/defcommand ^{:command-name "conf"} configure ...
... though there's rarely a need to ever do this.
Thedefcommand
macro works by adding the following meta-data to the Var it creates:
:command
(boolean) -- indicates the function is a command
:command-name
(string, optional) -- overrides the name of the command, normally the symbol
:doc
(string) -- the command's docstring is the command description, used when printing help
:command-opts
- a list of option specs, passed to joker.tools.cli/parse-opts
:command-args
- a list of positional argument specs
The command function is ultimately passed the command map containing two sub-maps:The :options map contains the command options, and the :argumentsmap contains a map of processed command line arguments;defcommand
adds alet
to pull values out of the command map and into theoption and argument symbols you define in the interface.
Other keys within the command map are private, used by multi.
Again,defcommand
is the preferred way to define commands.
multi
is (c) 2019-present Howard M. Lewis Ship.
It is released under the terms of the Apache Software License, 2.0.