This section details the use of the actual API presented by CLIFF.
com.djhaskin.cliff
This package's main export is the function execute-program
, but it also
exports other convenience functions listed below, as well as re-exporting all
the public symbols of the com.djhaskin.cliff/errors
package.
execute-program
execute-program
(program-name &key (cli-arguments t) (err-strm *error-output*) (list-sep ,) (map-sep =) (setup (function identity)) (strm *standard-output*) (teardown (function identity)) cli-aliases default-func-help default-function defaults disable-help environment-aliases environment-variables reference-file root-path subcommand-functions subcommand-helps suppress-final-output)
Overview
The function execute-program
aims to be a simple to use one stop shop for
all your command line needs.
The function gathers options from the "Option Tower", or out of
configuration files, environment variables, and the command line arguments
into an options table. Then it calls the action function, which is a
user-defined function based on what subcommand was specified; either the
default-function
if no subcommands were given, or the function
corresponding to the subcommand as given in subcommand-functions
will be called. Expects that function to return a results map, with at least
the :status
key set to one of the values listed in the *exit-codes*
hash table.
The Options Tower
The function first builds, in successive steps, the options table which will be passed to the function in question.
Configuration Files
It starts with the options hash table given by the defaults
parameter.
The function next examines the given environment-variables
,
which should be given as an alist with keys as variable names and values as
their values. If no list is given, the current environment variables will
be queried from the OS.
Then execute-program
uses those environment variables to find an
OS-specific system-wide configuration file in one of the following locations:
- Windows:
%PROGRAMDATA%\<program-name>\config.nrdl
, orC:\ProgramData\<program-name>\config.nrdl
if that environment variable is not set. - Mac:
/Library/Preferences/<program-name>/config.nrdl
- Linux/POSIX:
/etc/<program-name>/config.nrdl
It reads this file and deserializes the options from it, merging them into the options table, overriding any options when they exist both in the map and the file.
Next, it looks for options in an OS-specific, user-specific home-directory-based configuration file in one of the following locations:
- Windows:
%LOCALAPPDATA%\<program-name>\config.nrdl
, or%USERPROFILE%\AppData\Local\<program-name>\config.nrdl
if that environment variable is not set. - Mac:
$HOME/Library/Preferences/<program-name>/config.nrdl
- Linux/POSIX:
$XDG_CONFIG_HOME/<program-name>/config.nrdl
, or$HOME/.config/<program-name>/config.nrdl
if XDG_CONFIG_HOME is not set.
When performing this search, execute-program
may signal an error of type
necessary-env-var-absent
if the HOME var is
not set on non-Windows environments and the USERPROFILE
variable if on
Windows.
If it finds a NRDL file in this location, it deserializes the contents and merges them into the options table, overriding options when they exist both in the map and the file.
Finally, it searches for the reference-file
in the
root-path
. If it can't find the reference-file
in
root-path
, it searches successively in all of
root-path
's parent directories.
If it finds such a file in one of these directories, it next looks for the
file .<program-name>.nrdl
in that exact directory where
reference-file
was found. If that file exists, execute-program
deserializes the contents and merges them into the options table, overriding
options when they exist both in the table and the file.
If reference-file
is not given, it is simply taken to be the
configuration file itself, namely .<program-name>.nrdl
. If
root-path
is not given, it is taken to be the present working
directory.
Environment Variables
Next, this function examines the environment-variables
for any
options given by environment variable. Again,
environment-variables
should be given as an alist with keys as
variable names and values as their values. If no list is given, the current
environment variables will be queried from the OS.
For each environment variable, it examines its form.
If the variable matches the
regular expression
^(<PROGRAM_NAME>)_(?P<opt>LIST|TABLE|ITEM|FLAG|NRDL)_(?P<arg>.*)$
, then
the variable's value will be used to add to the resulting options hash table,
overriding any options which are already there.
If the opt
part of the regex is LIST
, the value of the variable will
be split using list-sep
and the resulting list of strings will be
associated with the keyword arg
in the options.
If the opt
is TABLE
, the value of the variable will be split using
list-sep
, then each entry in that list will also be split using
map-sep
. The resulting key/value pair list is turned into a
hash-table and this hash table is associated to the keyword arg
in the
options.
If the opt
is ITEM
, the value of the variable will be set to the
keyword arg
in the options.
If the opt
is NRDL
, the value of the variable will be parsed as a NRDL
string and its resultant value set as the value of the keyword arg
in the
returned options hash table.
In addition, any environment variables whose names match any keys in the
environment-aliases
alist will be treated as if their names were
actually the value of that key's entry.
Command Line Arguments
Finally, execute-program
turns its attention to the command line.
It examines each argument in turn. It looks for options of the form
--<action>-<option-key>
, though this is configurable via *find-tag*
.
It deals with options of this form according to the following rules:
- If the argument's action is
enable
ordisable
the keyword named after the option key is associated witht
ornil
in the resulting hash table, respectively. - If the argument's action is
set
, the succeeding argument is taken as the string value of the key corresponding to the option key given in the argument, overriding any previously set value within the option table. - If the argument's action is
add
, the succeeding argument is taken as a string value which must be appended to the value of the option key within the option table, assuming that the value of such is already a list. - If the argument's action is
join
, the succeeding argument must be of the form<key><map-sep><value>
, wheremap-sep
is the value of the parametermap-sep
. The key specified becomes a keyword, and the value a string, set as a hash table entry of the hash table found under the option key within the parent option table, assuming the value of such is already a hash table. - If the argument's action is
nrdl
, the succeeding argument must be a valid NRDL document, specified as a string. This argument's deserialized value will be taken as the value of the option key within the option map, overriding any previously set value within the option table. - If the argument's action is
file
, it will be assumed that the succeeding argument names a resource consumable viadata-slurp
. That resource will be slurped in via that function, then deserialized from NRDL. The resulting data will be taken as the value of the option key within the option table, overriding any previously set value within that table. - If the argument's action is
raw
, it will be assumed that the succeeding argument names a resource consumable viadata-slurp
. That resource will be slurped in via that function, as a raw string. That string will be taken as the value of the option key within the option table, overriding any previously set value within that table. - If the argument's action is
reset
, the option key in question is removed from the option table.
In addition, any argument of any form whose string value equal
's
any keys in the cli-aliases
alist will be treated as if their
string value were actually the value of that key's entry.
The Setup Function
Finally, if a setup function is specified via setup
, it is called
with one argument: the options table so far. This function is expected to
add or remove elements from the options table and return it.
Having done all this, execute-program
considers the option table is
complete and prepares to feed it to the action function.
Determining the Action Function
Any other arguments which execute-program
finds on the command line other
than those recognized either as cli-aliases
or as options of the
form matched by *find-tag*
will be taken as subcommand terms.
execute-program
then finds all such terms puts them in a list in the
order in which they were found. It attempts to find this list (using
equal
) in the alist subcommand-functions
. If such a list
exists as a key in that alist, the value corresponding to that key is taken
to be the action function. All action functions must be a function of one
hash table as an argument. This will be the options map previously constructed.
If no subcommand was given, the default-function
is taken as the
action function instead.
The Help Page
By default, if execute-program
sees the help
subcommand on in the
command line arguments, it will print a help page to err-strm
.
err-strm
may be given as t
, nil
, or otherwise must be a
stream, just as when calling format
. If left unspecified,
err-strm
defaults to standard error. This behavior may be
suppressed by setting the disable-help
option to nil
. If
disabled, Users may then define their own help pages by specifying functions
that print them using subcommand-functions
.
This help page gives users the following information:
- Details to users how they may specify options using the Options Tower for the program
- Lists all defined environment variable aliases
- Lists all defined command line interface aliases
- Prints out all options found within the Options Tower in NRDL format.
- Prints out whether there is a default action (function) defined.
- Prints out all available subcommands
If there were any subcommand terms after that of the help
term in the
command line arguments, they are put in a list and execute-program
attempts to find this list (again, using equal
) as a key in the
alist subcommand-helps
. It then prints this help string as part of
the documentation found in the help page. If it If there were no such terms
after the help
term in the command line arguments, execute-program
prints the help string found in default-help
, if any.
Execution
If execute-program
is able to determine an action function and
what options to put in the option table, it calls that function with the
found options. This function is either that which prints the default help
page as described above, it comes from subcommand-functions
and
was chosen based on present subcommand terms on the command line, or comes
from default-function
if no subcommand was given on the command
line. If no match was found in subcommands-functions
matching the
subcommands given on the command line, an error is printed. This function is
called the action function.
It computes the result hash table by taking the return value value of the
action function and passing it to the function specified in the
teardown
parameter, if it was given. If not, the result from the
action function is taken as the result hash table itself.
By default, it then prints this hash table out to strm
as a
prettified NRDL document. strm
may be given as t
, nil
, or
otherwise must be a stream, just as when calling format
. If left
unspecified, strm
defaults to standard output.
This return value is expected to be a hash table using eql
semantics. That table must contain at least one value under the :status
key. The value of this key is expected to be one of the keys found in the
*exit-codes*
alist corresponding to what should be the exit status of the
whole program. If the function was successful, the value is expected to be
:successful
. This value will be used as the key to look up a numeric exit
code from c(*exit-codes*). The numeric exit code found will be taken as the
desired exit code of the whole program, and will be the first value returned
by the function execute-program
. The second value will be the result hash
table itself.
Error Handling
During the entirety of its run, execute-program
handles any and all
serious-condition
s. If one is signaled, it computes the exit status
of the condition using exit-status
and creates a final result vector
containing the return value of that function under the :status
key. It
then populates this table with a key called error-message
and any
key/value pairs found in the alist computed by calling exit-map-members
on the condition.
It prints this table out in indented NRDL format to err-strm
(or
standard error if that option is left unspecified) unless
suppress-output
is given as t
.
execute-program
then returns two values: the numeric exit code
corresponding to the exit status computed as described above, and the newly
constructed result map containing the error information.
Discussion
Command line tools necessarily need to do a lot of I/O. execute-program
attempts to encapsulate much of this I/O while providing
clean
architecture by default. Ideally, execute-program
should enable an
action function to be relatively pure, taking an options hash table and
returning a result hash table with no other I/O required. This is why
execute-program
prints out the resulting hash table at the end. If a pure
action function is called, hooking it up to a subcommand or as the default
command action using execute-program
should enable this function to
interact with the outside world by means of its result table.
It was also written with dependency injection, testing, and the REPL in mind.
Since execute-program
doesn't actually exit the process at the end, only
returning values instead, execute-program
may simply be called at the
REPL. Many arguments to the function only exist for dependency injection,
which enables both testing and REPL development. Generally, many arguments
won't be specified in a function call, such as the cli-arguments
,
environment-variables
, err-strm
and strm
parameters (though of course they may be specified if e.g. the user needs to
redirect output to a file at need.)
ensure-option-exists
ensure-option-exists
(key options)
data-slurp
data-slurp
(resource &rest more-args)
Slurp a resource, using specified options. Return the contents of the resource as a string.
If resource
is a URL, download the contents according to the
following rules:
- If it is of the form `http(s)://user:password@url`, it performs basic HTTP authentication using the provided username and password;
- If it is of the form `http(s)://header=val@url`, the provided header is set when downloading the contents;
- If it is of the form `http(s)://<token>@url`, bearer authorization is used with the provided token;
- If it is of the form
file://<location>
, it is loaded as a normal file; - If it is of the form
-
the contents are loaded from standard input;
Otherwise, the contents are loaded from the resource as if it named a file.
parse-string
generate-string
find-file
find-file
(from marker)
Starting at the directory from
, look for the file
marker
.
Continue looking for the file in successive parents of from
until no more parents exist or the file is found.
Returns the pathname of the found file or nil if no file could be found.
necessary-env-var-absent
necessary-env-var-absent
Option | Value |
Superclasses: | (error t) |
Metaclass: | sb-pcl::condition-class |
Default Initargs: | nil |
Condition used by CLIFF to signal that a required environment variable is not present.
Implmements exit-status
and
exit-map-members
.
env-var
Environment variable that should exist (but doesn't).Option Value Allocation: instance Type: nil
Initarg: :env-var
Initform: (error "Need to give argument `:env-var`.")
Readers: (env-var)
invalid-subcommand
invalid-subcommand
Option | Value |
Superclasses: | (error t) |
Metaclass: | sb-pcl::condition-class |
Default Initargs: | nil |
Condition used by CLIFF to signal there were no functions given to CLIFF that correspond the subcommand given.
Implmements exit-status
and
exit-map-members
.
given-subcommand
Subcommand terms found on the command line.Option Value Allocation: instance Type: nil
Initarg: :given-subcommand
Initform: nil
Readers: (given-subcommand)
*find-tag*
*find-tag*
cl-ppcre
regex scanner
containing two capture groups, the first of which must capture the CLI verb
(one of enable
, disable
, reset
, add
, set
, nrdl
, or
file
), and the second of which is the name of the variable. This is used
ultimately by execute-program
to recognize command line options (as
opposed to subcommands). Currently its value corresponds to the regular
expression "^--([^-]+)-(.+)$"
.
com.djhaskin.cliff/errors
*exit-codes*
*exit-codes*
This parameter points to a hash table mapping keywords used by CLIFF
to numeric error codes. These error codes and their names are taken from
Linux's /usr/include/sysexit.h
in an attempt to be somewhat compliant to
that OS's standard.
Here are its contents:
Exit Code Name | Exit Code |
:unknown-error | 128 |
:general-error | 1 |
:successful | 0 |
:cl-usage-error | 64 |
:data-format-error | 65 |
:no-input-error | 66 |
:no-user-error | 67 |
:no-host-error | 68 |
:service-unavailable | 69 |
:internal-software-error | 70 |
:system-error | 71 |
:os-file-error | 72 |
:cant-create-file | 73 |
:input-output-error | 74 |
:temporary-failure | 75 |
:protocol-error | 76 |
:permission-denied | 77 |
:configuration-error | 78 |
exit-status
exit-status
(condition)
Return a keyword describing the program exit status implied by a given
condition. This keyword must be in *exit-codes*
.
Users may define their own methods to this function.
exit-map-members
exit-map-members
(condition)
Return an alist of items to be added to the exit map of CLIFF in the event
of the condition in question being caught by execute-program
.
This generic function has been implemented for all standard Common Lisp
condition
types.
Users may define their own methods to this function. Arbitrary mappings between keywords and NRDL-serializable objects are allowed in the resulting alist.