utility-arguments

2016-12-04

Utility Arguments

Utility Arguments is a Common Lisp software that handles command-line arguments. The adopted argument syntax is described in the POSIX and the GNU conventions.

Features

  • Real notion of a usage, which allows Utility Arguments to automatically select a suitable usage for operands and options.

  • Robust in parsing command-line arguments. Utility Arguments doesn't trip on spaces in option specifications, and options are not required to appear before operands.

  • Lispy feel. Lambda lists describe the operands and options a usage binds ? this idea was taken from Command Line Arguments.

  • No auto-generated documentation for operands, options, and usages; because it would be hard to produce a style which pleases all eyes.

Dependencies

Installation

Utility Arguments is hosted on Gitlab.com. The master branch points to the most recent release.

The easiest way to install Utility Arguments is through Quicklisp.

Alternatively, either download a tarball from the project's website or clone the Git repository. Put the source code where ASDF can find it. The system utility-arguments can then be loaded and compiled.

Overview

Utility arguments are formatted strings that conform to the POSIX and the GNU argument syntax conventions. To summarize it here: Option specifications start with one or two hyphens followed by alphanumeric characters. Everything that is not an option specification is an operand. A double hyphen terminates options, i.e., everything to the right of it is considered an operand.

Commonly, the utility arguments will be the arguments received on the command-line. To retrieve the command-line arguments in a portable way, use the function uiop:command-line-arguments.

A usage, which is defined with the macro defusage, can be thought of as a Lisp function applicable to a set of utility arguments. Its positional and keyword parameters receive the values of operands and options. Type parsing can be requested through objects handed over to the function apply-usage; otherwise, a string trimmed of white space at both ends is passed as value.

Usages are applied to utility arguments with the function apply-usage. Among other arguments, it receives two lists of operand and option objects that complement the parameters on usage lambda lists in providing additional type and syntactic information.

A usage is applicable to a set of utility arguments if there is a correlated parameter for each operand and option, and if all parameters are satisfied on the usage lambda list.

API

Symbols exported from the package utility-arguments comprise the API and are documented by their docstrings. The examples below cover the majority of the API.

Example

In this example we create the outline for a hypothetical encryption utility to demonstrate how to use Utility Arguments.

We devise usages to encrypt and decrypt files. The usages are implemented with the macro defusage. Positional parameters on the first lambda list describe operands, and keyword parameters on the second lambda list describe options. Parameters receive the values of operands and options.

(defusage encrypt ((infile &optional outfile)
                   (&key encrypt (algorithm 'aes) (keysize 256)))
  (declare (ignore encrypt))
  ;; code...
  )

(defusage decrypt ((infile &optional outfile)
                   (&key decrypt algorithm keysize))
  (declare (ignore decrypt))
  ;; code...
  )

For the encryption usage we made algorithm and keysize optional options by specifying an init-form for the keyword parameters.

We have no real use for the encrypt and decrypt parameters inside the body of the usages; however, they are essential for the usages to be distinguishable from each other.

We create operand objects that complement the positional parameters on the usage lambda lists. Correlation is established by option key and parameter name. For type we pass a function object instead of a Lisp type specifier, which gives us full control over the received argument.

(defun probe-infile (string designator)
  (when (not (uiop:probe-file* string))
    (warn "File ~s specified for ~a does not exist." string designator))
  string)

(defun probe-outfile (string designator)
  (when (uiop:probe-file* string)
    (warn "File ~s specified for ~a does already exist." string designator))
  string)

(defparameter *operands*
  (list (make-operand :key 'infile  :type #'probe-infile)
        (make-operand :key 'outfile :type #'probe-outfile)))

We create option objects that complement the keyword parameters on the usage lambda lists. Correlation is established by option key and parameter name. Different kind of options are available. We specify key, short and long designators, and for options that receive an argument a Lisp type specifier. For type parsing, the Lisp reader is used.

(defparameter *options*
  (list (make-option-flag
         :key :encrypt :short #\e :long "encrypt")
        (make-option-flag
         :key :decrypt :short #\d :long "decrypt")
        (make-option-with-argument
         :key :algorithm :short #\a :long "algorithm" :type '(or (eql aes) (eql des)))
        (make-option-with-argument
         :key :keysize :short #\s :long "keysize" :type '(integer 128))))

We apply the usages to utility arguments with the function apply-usage. The next two examples succeed.

(apply-usage '(encrypt decrypt) *operands* *options*
             (list "--encrypt" "secret.txt" "secret.enc"))

(apply-usage '(encrypt decrypt) *operands* *options*
             (list "--decrypt" "secret.enc" "-aAES" "--keysize=256"))

This example signals a utility-argument-error condition, because we didn't make any provisions for an option --help.

(apply-usage '(encrypt decrypt) *operands* *options*
             (list "--help"))

This example signals a usage-error condition, because we did not devise such usage. We need at least an input-file operand for this to succeed.

(apply-usage '(encrypt decrypt) *operands* *options*
             (list "--encrypt"))

The reason defusage takes two lambda lists instead of one is to allow for a rest parameter to be specified for operands and options each, like in the next example.

(defusage foo ((&rest ints)
               (&rest options &key x &allow-other-keys))
  ;; code...
  )

To successfully apply the usage, we need to provide the option x. Additionally any other option and integer operands are accepted.

(apply-usage '(foo)
             (list (make-operand :key 'ints :type ' (integer * 0)))
             (list (make-option-flag :key :x :short #\x)
                   (make-option-flag :key :y :short #\y))
             (list "-xy" "--" "-3" "-7"))
 
Author
Frank A. U.
License
ICS