docs/01-usage.markdown @ bc8760190ed7

Remove unused dependency from adopt/test
author Steve Losh <steve@stevelosh.com>
date Mon, 01 Jul 2019 16:02:40 -0400
parents 860e10544652
children 7f0abaf11050
Usage
=====

Adopt is a simple library for parsing UNIX-style command line arguments in
Common Lisp.  It was made because none of the other libraries did what I needed.

[TOC]

Package
-------

All core Adopt functions are in the `adopt` package.  Several of the symbols in
adopt shadow those in the `common-lisp` package, so you should probably use
namespaced `adopt:…` symbols instead of `USE`ing the package.

Interfaces
----------

To get started with Adopt, you should create an interface with the
`adopt:make-interface` function.  This returns an object representing the
command line interface presented to your users.

### Creating an Interface

Let's say you're developing a program to search the contents of files (because
the world certainly needs *another* `grep` replacement).  You might start with
something like:

    (defparameter *ui*
      (adopt:make-interface
        :name "search"
        :summary "search files for a regular expression"
        :usage "[OPTIONS] PATTERN [FILE]..."
        :help "Search the contents of each FILE for the regular expression PATTERN.  If no files are specified, searches standard input instead."))

You can now print some help text for your CLI with `adopt:print-help`:

    (adopt:print-help *ui*)
    ; =>
    search - search files for a regular expression

    USAGE: … [OPTIONS] PATTERN [FILE]...

    Search the contents of each FILE for the regular expression PATTERN.  If no
    files are specified, searches standard input instead.

### Line Wrapping

Adopt will handle line-wrapping your help text, so you don't need to (and
shouldn't) add extra line breaks when creating your interface.  If you want to
line break the text in your source code to fit nicely in your editor, remember
that `adopt:make-interface` is just a function — you can use `format` (possibly
with its `~Newline` directive) to preprocess the help text argument:

    (defparameter *ui*
      (adopt:make-interface
        :name "search"
        :summary "search files for a regular expression"
        :usage "[OPTIONS] PATTERN [FILE]..."
        :help (format nil "Search the contents of each FILE for ~
                           the regular expression PATTERN.  If ~
                           no files are specified, searches ~
                           standard input instead.")))

Adopt's line-wrapping library [Bobbin][] will only ever *add* line breaks, never
remove them, which means you can include breaks in the output if you want to
have multiple paragraphs in your help text:

    (defparameter *ui*
      (adopt:make-interface
        :name "search"
        :summary "search files for a regular expression"
        :usage "[OPTIONS] PATTERN [FILE]..."
        :help (format nil
                "Search the contents of each FILE for the regular expression PATTERN.~@
                 ~@
                 If no files are specified (or if - is given as a file name) standard input will be searched instead.")))

If you want to control the width of the help text lines when they are printed,
`adopt:print-help` takes a `:width` argument:

    (adopt:print-help *ui* :width 50)
    ; =>
    search - search files for a regular expression

    USAGE: … [OPTIONS] PATTERN [FILE]...

    Search the contents of each FILE for the regular
    expression PATTERN.

    If no files are specified (or if - is given as a
    file name) standard input will be searched
    instead.

`adopt:print-help` takes a number of other options — see the API Reference for
more information.

### Examples

Describing the CLI in detail is helpful, but users can often learn a lot more by
seeing a few examples of its usage.  `adopt:make-interface` can take an
`:examples` argument, which should be an alist of `(description . example)`
conses:

    (defparameter *ui*
      (adopt:make-interface
        :name "search"
        :summary "search files for a regular expression"
        :usage "[OPTIONS] PATTERN [FILE]..."
        :help …
        :examples
        '(("Search foo.txt for the string 'hello':"
           . "search hello foo.txt")
          ("Search standard input for lines starting with x:"
           . "search '^x' -")
          ("Watch the file log.txt for lines containing the username steve.losh:"
           . "tail foo/bar/baz/log.txt | search --literal steve.losh -"))))

    (adopt:print-help *ui* :width 50)
    ; =>
    search - search files for a regular expression

    USAGE: … [OPTIONS] PATTERN [FILE]...

    Search the contents of each FILE for the regular
    expression PATTERN.

    If no files are specified (or if - is given as a
    file name) standard input will be searched
    instead.

    Examples:

      Search foo.txt for the string 'hello':

          search hello foo.txt

      Search standard input for lines starting with x:

          search '^x' -

      Watch the file log.txt for lines containing the
      username steve.losh:

          tail foo/bar/baz/log.txt | search --literal steve.losh -

Notice how Adopt line wraps the prose explaining each example, but leaves the
example itself untouched for easier copying and pasting.  In general Adopt tries
to do the right thing for your users (even when that means making a little more
work for *you* in certain places).

Exiting
-------

Adopt provides some helpful utility functions to exit out of your program with
a UNIX exit code.  These do what you think they do:

    (adopt:exit)

    (adopt:exit 1) 

    (adopt:print-help-and-exit *ui*)

    (adopt:print-help-and-exit *ui*
      :stream *error-output*
      :exit-code 1)

    (handler-case (assert (= 1 0))
      (error (err)
        (adopt:print-error-and-exit err)))

These functions are not implemented for every Lisp implementation.  PRs are
welcome, or you can just write the implementation-specific calls in your program
yourself.

Options
-------

Now that you know how to create an interface, you can create some options to use
inside it with `adopt:make-option`:

    (defparameter *option-version*
      (adopt:make-option 'version
        :long "version"
        :help "display version information and exit"
        :reduce (constantly t)))

    (defparameter *option-help*
      (adopt:make-option 'help
        :long "help"
        :short #\h
        :help "display help information and exit"
        :reduce (constantly t)))

    (defparameter *option-literal*
      (adopt:make-option 'literal
        :long "literal"
        :short #\l
        :help "treat PATTERN as a literal string instead of a regular expression"
        :reduce (constantly t)))

    (defparameter *ui*
      (adopt:make-interface
        :name "search"
        :summary "search files for a regular expression"
        :usage "[OPTIONS] PATTERN [FILE]..."
        :help "Search the contents of …"
        :contents (list
                    *option-version*
                    *option-help*
                    *option-literal*)))

Adopt will automatically add the options to the help text:

    (adopt:print-help *ui*)
    ; =>
    search - search files for a regular expression

    USAGE: /usr/local/bin/sbcl [OPTIONS] PATTERN [FILE]...

    Search the contents of …

    Options:
      --version             display version information and exit
      -h, --help            display help information and exit
      -l, --literal         treat PATTERN as a literal string instead of a regular
                            expression

The first argument to `adopt:make-option` is the name of the option, which we'll
see put to use shortly.  At least one of `:short` and `:long` is required, and
`:help` text must be specified.  We'll talk more about `:reduce` in a bit, but
it too is required.

I prefer to define each option as its own global variable to keep the call to
`adopt:make-interface` from getting too large and unwieldy, but feel free to do
something like this if you prefer to avoid cluttering your package:

    (defparameter *ui*
      (adopt:make-interface

        :contents
        (list (adopt:make-option 'foo …)
              (adopt:make-option 'bar …)
              …)))

Parsing
-------

At this point we've got an interface with some options, so we can use it to
parse a list of strings we've received as command line arguments:

    (adopt:parse-options *ui* '("foo.*" "--literal" "a.txt" "b.txt"))
    ; =>
    ("foo.*" "a.txt" "b.txt")
    #<HASH-TABLE :TEST EQL :COUNT 3 {10103142A3}>

`adopt:parse-options` returns two values: a list of non-option arguments, and
a hash table of the option values.

The keys of the hash table are (by default) the option names given as the first
argument to `adopt:make-option`.  We'll see how the option values are determined
soon.

From now on I'll use a special pretty printer for hash tables to make it easier
to see what's inside them:

    (adopt:parse-options *ui* '("foo.*" "--literal" "a.txt" "b.txt"))
    ; =>
    ("foo.*" "a.txt" "b.txt")
    {LITERAL: T, VERSION: NIL, HELP: NIL}

Top-Level Structure
-------------------

We'll look at how the option values are computed shortly, but first let's see
the overall structure of the programs you create with Adopt:

    (defun toplevel ()
      (handler-case
          (multiple-value-bind (arguments options)
              (adopt:parse-options *ui*)
            (when (gethash 'help options)
              (adopt:print-help-and-exit *ui*))
            (when (gethash 'version options)
              (format t "1.0.0~%")
              (adopt:exit))
            (destructuring-bind (pattern . files) arguments
              (run pattern
                   files
                   :literal (gethash 'literal options))))
        (error (c)
          (adopt:print-error-and-exit c))))

    (sb-ext:save-lisp-and-die "search" :toplevel #'toplevel)

The `toplevel` function first uses a `handler-case` to trap all `error`s.  If
any error occurs it will print the error message and exit, to avoid confusing
users by dropping them into a Lisp debugger REPL (which they probably won't
understand).  When you're developing your program yourself you'll want to omit
this part and let yourself land in the debugger as usual.

Next we use `adopt:parse-options` to parse the command line arguments and
options.  We do some initial checks to see if the user wants `--help` or
`--version` information.  If not, we destructure the arguments into the items we
expect and call a `run` function with all the information it needs to do its
job.

If the `destructuring-bind` fails an error will be signaled, and the
`handler-case` will print it and exit.  If you want to be a nice person you
could check that the `arguments` have the correct shape first, and return
a friendlier error message to your users if they don't.

Computing Values with Reduce
----------------------------