CLIFF: Command Line Interface Functional Framework ยป API Reference

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, or C:\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 or disable the keyword named after the option key is associated with t or nil 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>, where map-sep is the value of the parameter map-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 via data-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 via data-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-conditions. 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)

Check options hash table options for the key key.

Signal a restartable condition if the option is missing. Employs the use-value and continue restarts in that case.

This function is meant to be used by the user to ensure an option exists.

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

parse-string(thing)
Parse the NRDL string thing.

generate-string

generate-string(thing &optional &key (pretty 0))
Serialize thing to NRDL.

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
OptionValue
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).
    OptionValue
    Allocation:instance
    Type:nil
    Initarg::env-var
    Initform:(error "Need to give argument `:env-var`.")
    Readers:(env-var)

invalid-subcommand

invalid-subcommand
OptionValue
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.
    OptionValue
    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.