The core of cli-tools
is the net.lewisship.cli-tools/defcommand
macro. This is how you define commands.
defcommand
defines a command as a special kind of function; where you would normally define the
parameters of an ordinary function, with defcommand
, you define the command line interface
of your command, including how those options and arguments are mapped to local symbols.
The interface defines options as well as positional arguments; those options and arguments are available
in the body of the command just as if they were parameters passed to the function.
defcommand
defines a function that accepts a variadic number of command line argument strings,
parses them as options and positional arguments, binds those to local symbols, and evaluates the body.
An example to begin; let's say you are creating a command for administrating some part of your application.
You need to know a URL to update, and a set of key/value pairs to configure. Let's throw in a --verbose
option just for kicks.
src/app_admin/commands.clj:
(ns app-admin.commands
"Command to administrate my service"
(:require [net.lewisship.cli-tools :refer [defcommand]]))
(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]]
; Placeholder:
(prn :verbose verbose :host host :key-values key-values))
The meat of this configure
command has been replaced with a call to prn
, but
the important part for this discussion is the interface, which takes the place of an ordinary
function's parameters declaration.
Initially, the interface is about options, and we define one option, --verbose
, a flag.
Inside the body, the value will be bound to local symbol verbose
, which will be nil if --verbose
is
not specified, or true if it is.
defcommand
always adds the -h
/ --help
flag, and implements it; the body does not get
evaluated if help is requested, or if there's any kind of validation error processing
command line arguments.
An option definition always starts with three strings: the short option name, the long option name, and the option description; these are positional, and a nil may be supplied.
The full details of an option definition are supplied by Clojure's tools.cli library.
The way positional arguments are defined is intended to be similar to how
options are defined by tools.cli
:
["<LABEL>" "<DOC>" <key/value pairs>]
The <LABEL>
is a string used in the summary, and in validation error messages;
the <DOC>
is a string used in the summary. After that come key/value pairs.
:optional
(boolean, default false) -- if true, the argument may be omitted if there isn't a
command line argument to match
:repeatable
(boolean, default false) -- if true, then any remaining command line arguments are processed
by the argument
:parse-fn
- passed the command line argument string, 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 argument 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.
Only the final positional argument may be repeatable.
Also note that all command line arguments must be consumed, either as options or as positional arguments. Any additional command line arguments will be reported as a validation error.
The interface vector of defcommand may have additional options; these are keywords that change
how following values in the interface vector are parsed.
We saw this in the example above, where :args
was used to switch from defining options to defining
positional arguments.
Indicates that any following terms define options; this is the initial parser state, so :options
is rarely used.
Indicates that any following terms define positional arguments.
Overrides the default name for the command, which is normally the same as the function name. This is useful, for example, when the desired command name would conflict with a clojure.core symbol, or something else defined within your namespace.
The :command
option is also useful when using cli-tools to define
the -main
function for a simple tool (a tool with options and arguments,
but not subcommands); this command name will be used in the command summary (from the
-h
/ --help
switch).
Normally, the command title (which appears next to the command in the help
tool summary) is just
the first sentence of the command's docstring, up to the first .
. If, for some reason,
that default is incorrect, the command's summary can be explicitly specified using :title
.
By default, options are parsed using clojure.tools.cli/parse-opts
, with the :in-order
option set to false;
this means that parse-opts
will stop at the first
option-like string that isn't declared.
(defcommand remote
"Use ssh to run a command remotely."
[verbose ["-v" "--verbose"]
:args
command ["COMMAND" "Remote command to execute"]
remote-args ["ARGS" "Arguments to remote command"
:optional true
:repeatable true]]
...)
You might expect that app-admin remote ls -lR
would work, but it will fail
with an error that -lR is not recognized
.
You can always use --
to split options from arguments, so app-admin remote -- ls -lR
will work,
but is clumsy.
Instead, add :in-order true
to the end of the interface, and any
unrecognized options will be parsed as positional arguments instead,
so app-admin remote ls -lR
will work, and -lR
will be provided as a string in the remote-args
seq.
It can be useful to define local symbols that can be referenced inside the option
and arguments definitions; the :let
keyword is followed by a vector of bindings.
(defcommand set-mode
"Sets the execution mode"
[mode ["-m" "--mode MODE" (str "Execution mode, one of " mode-names)
:parse-fn keyword
:validate [allowed-modes (str "Must be one of " mode-names)]]
:let [allowed-modes #{:batch :async :real-time}
mode-names (->> allowed-modes (map name) sort (string/join ", "))]]
...)
Note that the
new.lewisship.cli-tools/select-option
function is an easier way to create such an option.
In the expanded code, the bindings are moved to the top, before the option and argument
definitions. Further, if there are multiple :let
blocks, they are concatinated.
This also means that the bindings can not reference the symbols for options or arguments.
Often you will need to perform validations that consider multiple fields.
The :validate
directive adds tests that occur after primary parsing of command line options
has occurred, but before executing the body of the function.
It is a vector of tests and messages.
Each test expression is evaluated in turn; if the result is falsey, then the message
is passed to print-errors
as an error, and exit
is called with the value 1.
A common case is to handle mutually exclusive arguments:
(defcommand sort-data
"Sorts some data"
[alpha ["-a" "--alpha-numeric" "Sort in alpha-numeric order"]
numeric ["-n" "--numeric" "Sort in numeric order"]
:validate [(not (and alpha numeric)) "Only one of --alpha-numeric or --numeric is allowed"]]
; At most one of alpha or numeric is true here
)
Note that unlike a validate function for an option or argument, these are expressions that can leverage
local symbols (such as alpha
and numeric
) and not functions that are passed a value.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
Ctrl+k | Jump to recent docs |
← | Move to previous article |
→ | Move to next article |
Ctrl+/ | Jump to the search field |