Adopt

Usage

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

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 USEing the package.

Interfaces

To get started with Adopt you can create an interface object with adopt:make-interface. 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:

:::lisp
(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."))

make-interface takes several required arguments:

You can now print some pretty help text for the CLI with adopt:print-help:

:::lisp
(adopt:print-help *ui*)
; =>
; search - search files for a regular expression
;
; USAGE: /path/to/binary [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 text 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:

:::lisp
(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.")))

If you want to pull out the documentation string into its own variable to keep that make-interface call from getting too unwieldy, you can certainly do that:

:::lisp
(defparameter *help-text*
  (format nil "Search the contents of each FILE for the ~
               regular expression PATTERN.  If no files ~
               are specified, searches standard input ~
               instead."))

(defparameter *ui*
  (adopt:make-interface
    :name "search"
    :summary "search files for a regular expression"
    :usage "[OPTIONS] PATTERN [FILE]..."
    :help *help-text*))

The (defparameter … (format nil …)) pattern can be tedious to write, so Adopt provides a helper macro define-string that does exactly that:

:::lisp
(adopt:define-string *help-text*
  "Search the contents of each FILE for the regular ~
   expression PATTERN.  If no files are specified, ~
   searches standard input instead.")

(defparameter *ui*
  (adopt:make-interface
    :name "search"
    :summary "search files for a regular expression"
    :usage "[OPTIONS] PATTERN [FILE]..."
    :help *help-text*))

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. Once again, format is your friend:

:::lisp
(adopt:define-string *help-text*
  "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.")

(defparameter *ui*
  (adopt:make-interface
    :name "search"
    :summary "search files for a regular expression"
    :usage "[OPTIONS] PATTERN [FILE]..."
    :help *help-text*))

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

:::lisp
(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.

Adding Examples

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

:::lisp
(defparameter *ui*
  (adopt:make-interface
    :name "search"
    :summary "search files for a regular expression"
    :usage "[OPTIONS] PATTERN [FILE]..."
    :help *help-text*
    :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:

:::lisp
(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 if you prefer.

Options

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

:::lisp
(defparameter *option-version*
  (adopt:make-option 'version
    :long "version"
    :help "Display version and exit."
    :reduce (constantly t)))

(defparameter *option-help*
  (adopt:make-option 'help
    :long "help"
    :short #\h
    :help "Display help 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:

:::lisp
(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 and exit.
;   -h, --help            Display help and exit.
;   -l, --literal         Treat PATTERN as a literal string instead of a regular
;                         expression.

The first argument to 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 little while, but it too is required.

When writing :help text I recommend using a full sentence, starting with a capital letter and ending with appropriate punctuation. If there's a default value or behavior for the option, mention it in the help text with something like Search at most N lines (default 100)..

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

:::lisp
(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 with adopt:parse-options:

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

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

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

parse-options returns two values:

  1. A list of non-option arguments.
  2. An eql hash table of the option keys and values.

We'll talk about how the option values are determined soon. The keys of the hash table are (by default) the option names given as the first argument to make-option. You can specify a different key for a particular option with the :result-key argument to make-option:

:::lisp
(defparameter *option-literal*
  (adopt:make-option 'literal
    :result-key 'pattern-is-literal
    :long "literal"
    :short #\l
    :help "Treat PATTERN as a literal string instead of a regular expression."
    :reduce (constantly t)))

;; …

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

This can come in useful if you want multiple options that affect the same result (e.g. --verbose and --silent flags that toggle extra log output on and off).

Option Formats

Adopt tries to support the most common styles of long and short UNIX options.

Long options must be given with two dashes (--foo). If a long option takes a parameter it can be given as the next argument (--foo meow) or mashed together into the same argument using an equals sign (--foo=meow).

Short options must be given with a single dash (-f). If several short options do not take any parameters, they can be mashed together and given all at once (-xzvf). If a short option takes a parameter it can be given as the next argument (-n foo) or mashed together with the option -nfoo.

The special string -- signals that all remaining arguments are normal text arguments, and should not be parsed as options.

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'll typically create with Adopt:

:::lisp
(defun run (pattern files &key literal)
  ;; Actually do something here.
  )

(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))))

(defun build ()
  (sb-ext:save-lisp-and-die "search" :executable t :toplevel #'toplevel))

This is a typical way to use Adopt. There are three important functions here:

In this example the toplevel function first uses a handler-case to trap all errors. 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). If you're developing a program just for yourself, you might want to omit this part and let yourself land in the debugger as usual.

Next it uses adopt:parse-options to parse the command line arguments and options. It them does some initial checks to see if the user wants --help or --version information. If so, it prints the requested information and exits.

Otherwise it destructures the arguments into the expected items and calls run 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

So far we've talked about how to define an interface, print help text, parse a list of options, and the overall structure of the program you'll create with Adopt. Now we need to talk about how the options the user specifies are parsed and turned into the resulting hash table.

Not all command-line options are the same. There are several common types of options in the UNIX world:

An option-parsing library needs to give you the tools to handle all of these cases (and more). Python's argparse library, for example, has a number of different "actions" to account to handle these various use cases. Adopt works differently: it uses an interface similar to reduce to let you do whatever you need.

First: before any options are parsed, all entries in the options hash table have their values set to the :initial-value given to make-option (or nil if none was specified).

Next: When you create an option you must specify a :reduce function that takes the current value (and, for options that take a parameter, the given parameter) and produces a new value each time the option is given.

You may also specify a :finally function that will be called on the final value after all parsing is done.

For convenience, if an option takes a parameter you may also specify a :key function, which will be called on the given string before it is passed to the :reduce function. For example: you might use this for an option that takes integers as arguments with something like :key #'parse-integer.

The combination of these four pieces will let you do just about anything you might want. Let's look at how to do some common option parsing tasks using these as our building blocks.

Simple Options

To define an option that just tracks whether it's ever been given, you can do something like:

:::lisp
(defparameter *option-help*
  (adopt:make-option 'help
    :long "help"
    :short #\h
    :help "Display help and exit."
    :initial-value nil
    :reduce (lambda (current-value)
              (declare (ignore current-value))
              t)))

But since nil is the default initial value and Common Lisp provides the handy constantly function, you can do this more concisely:

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

Boolean Options

If you want to have multiple options that both affect the same key in the results, you can use :result-key to do this:

:::lisp
(defparameter *option-paginate*
  (adopt:make-option 'paginate
    :long "paginate"
    :short #\p
    :help "Turn pagination on."
    :reduce (constantly t)))

(defparameter *option-no-paginate*
  (adopt:make-option 'no-paginate
    :result-key 'paginate
    :long "no-paginate"
    :short #\P
    :help "Turn pagination off (the default)."
    :reduce (constantly nil)))

The way we've written this, if the user gives multiple options the last-given one will take precedence. This is generally what you want, because it allows someone to add a shell alias with these options like this:

:::bash
alias g='git --paginate --color=always'

but still lets them override an option at runtime for a single invocation:

:::bash
g --no-paginate log
# expands to: git --paginate --color=always --no-paginate log

If the last-given option didn't take precedence, they'd have to fall back to the non-alias version of the command, and type out all the options they do want by hand. This is annoying, so it's usually better to let the last one win.

Making two separate options by hand can be tedious if you have a lot of boolean options, so Adopt provides a make-boolean-options function that will do some of the boilerplate for you:

:::lisp
(adopt:make-boolean-options 'paginate
  :long "paginate"
  :short #\p
  :help "Turn pagination on."
  :help-no "Turn pagination off (the default).")
;; =>
#<ADOPT::OPTION PAGINATE p/paginate>
#<ADOPT::OPTION NO-PAGINATE P/no-paginate>

make-boolean-options will try to guess at sensible values to reduce the boilerplate you need to type:

The two options are returned as separate values. Adopt also provides a defparameters convenience macro to create special variables for them more easily:

:::lisp
(defparameters (*option-paginate* *option-no-paginate*)
  (adopt:make-boolean-options 'paginate
    :long "paginate"
    :short #\p
    :help "Turn pagination on."
    :help-no "Turn pagination off (the default)."))

Counting Options

To define an option that counts how many times it's been given, like SSH's -v, you can use something like this:

:::lisp
(defparameter *option-verbosity*
  (adopt:make-option 'verbosity
    :short #\v
    :help "Output more verbose logs."
    :initial-value 0
    :reduce #'1+))

Single-Parameter Options

To define an option that takes a parameter and only keeps the last one given, you can do something like:

:::lisp
(defparameter *option-repository*
  (adopt:make-option 'repository
    :parameter "PATTERN"
    :long "repository"
    :short #\R
    :help "Path to the repository (default .)."
    :initial-value "."
    :reduce (lambda (prev new)
              (declare (ignore prev))
              new)))

Specifying the :parameter argument makes this option a parameter-taking option, which means the :reduce function will be called with the current value and the given parameter each time.

Writing that lambda out by hand every time would be tedious. Adopt provides a function called last (as in "keep the last parameter given") that does exactly that:

:::lisp
(defparameter *option-repository*
  (adopt:make-option 'repository
    :long "repository"
    :short #\R
    :help "Path to the repository (default .)."
    :initial-value "."
    :reduce #'adopt:last))

Multiple-Parameter Options

Collecting every parameter given can be done in a number of different ways. One strategy could be:

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN (may be given multiple times)."
    :initial-value nil
    :reduce (lambda (patterns new)
              (cons new patterns))))

You might notice that the :reduce function here is just cons with its arguments flipped. Common Lisp doesn't have a function like Haskell's flip, so Adopt provides it:

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN (may be given multiple times)."
    :initial-value nil
    :reduce (adopt:flip #'cons)))

Note that the result of this will be a fresh list of all the given parameters, but their order will be reversed because cons adds each new parameter to the front of the list. If the order doesn't matter for what you're going to do with it, you're all set. Otherwise, there are several ways to get around this problem. The first is to add the parameter to the end of the list in the :reduce function:

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN (may be given multiple times)."
    :initial-value nil
    :reduce (lambda (patterns new)
              (append patterns (list new)))))

This is tedious and inefficient if you have a lot of arguments. If you don't care much about argument parsing speed, Adopt provides a function called collect that does exactly this, so you don't have to type out that lambda yourself (and nil is the default initial value, so you don't need that either):

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN (may be given multiple times)."
    :reduce #'adopt:collect))

A more efficient (though slightly uglier) solution would be to use nreverse at the end:

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN (may be given multiple times)."
    :reduce (adopt:flip #'cons)
    :finally #'nreverse))

If you really need maximum efficiency when parsing command line options (you probably don't) you could use a queue library, or use a vector and vector-push-extend, or anything else you might dream up. The combination of :reduce, :initial-value, and :finally will let you do just about anything.

Required Options

Adopt doesn't have a concept of a required option. Not only is "required option" an oxymoron, but it's almost never what you want — if a user types foo --help they shouldn't get an error about a missing required option.

In cases where you really do need to require an option (perhaps only if some other one is also given) you can check it yourself:

:::lisp
(defun toplevel ()
  (handler-case
      (multiple-value-bind (arguments options) (adopt:parse-options *ui*)
        (when (gethash 'help options)
          (adopt:print-help-and-exit *ui*))
        (unless (gethash 'some-required-option options)
          (error "Required option foo is missing."))
        (run …))
    (error (c)
      (adopt:print-error-and-exit c))))

Option Groups

Related options can be grouped together in the help text to make them easier for users to understand. Groups can have their own name, title, and help text.

Here's a example of how this works. It's fairly long, but shows how Adopt can help you make a command line interface with all the fixins:

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

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

(defparameter *option-no-literal*
  (adopt:make-option 'no-literal
    :result-key 'literal
    :help "Treat PATTERN as a regex (the default)."
    :long "no-literal"
    :short #\L
    :reduce (constantly nil)))

(defparameter *option-case-sensitive*
  (adopt:make-option 'case-sensitive
    :help "Match case-sensitively (the default)."
    :long "case-sensitive"
    :short #\c
    :initial-value t
    :reduce (constantly t)))

(defparameter *option-case-insensitive*
  (adopt:make-option 'case-insensitive
    :help "Ignore case when matching."
    :long "case-insensitive"
    :short #\C
    :result-key 'case-sensitive
    :reduce (constantly nil)))

(defparameter *option-color*
  (adopt:make-option 'color
    :help "Highlight matches with color."
    :long "color"
    :reduce (constantly t)))

(defparameter *option-no-color*
  (adopt:make-option 'no-color
    :help "Don't highlight matches (the default)."
    :long "no-color"
    :result-key 'color
    :reduce (constantly nil)))

(defparameter *option-context*
  (adopt:make-option 'context
    :parameter "N"
    :help "Show N lines of context (default 0)."
    :long "context"
    :short #\U
    :initial-value 0
    :reduce #'adopt:last
    :key #'parse-integer))


(defparameter *group-matching*
  (adopt:make-group 'matching-options
    :title "Matching Options"
    :options (list *option-literal*
                   *option-no-literal*
                   *option-case-sensitive*
                   *option-case-insensitive*)))

(defparameter *group-output*
  (adopt:make-group 'output-options
    :title "Output Options"
    :help "These options affect how matching lines are printed.  The defaults are ideal for piping into other programs."
    :options (list *option-color*
                   *option-no-color*
                   *option-context*)))


(adopt:define-string *help-text*
  "Search FILEs for lines that match the regular expression ~
   PATTERN and print them to standard out.  Several options ~
   are available to control how the matching lines are printed.~@
   ~@
   If no files are given (or if - is given as a filename) ~
   standard input will be searched.")

(defparameter *ui*
  (adopt:make-interface
    :name "search"
    :usage "PATTERN [FILE...]"
    :summary "print lines that match a regular expression"
    :help *help-text*
    :contents (list *option-help*
                    *group-matching*
                    *group-output*)))

And with all that out of the way, you've got some nicely-organized help text for your users:

:::lisp
(adopt:print-help *ui* :width 60 :option-width 16)
; =>
; search - print lines that match a regular expression
;
; USAGE: /usr/local/bin/sbcl PATTERN [FILE...]
;
; Search FILEs for lines that match the regular expression
; PATTERN and print them to standard out.  Several options are
; available to control how the matching lines are printed.
;
; If no files are given (or if - is given as a filename)
; standard input will be searched.
;
; Options:
;   -h, --help        display help and exit
;
; Matching Options:
;   -l, --literal     treat PATTERN as a literal string
;                     instead of a regex
;   -L, --no-literal  treat PATTERN as a regex (the default)
;   -c, --case-sensitive
;                     match case-sensitively (the default)
;   -C, --case-insensitive
;                     ignore case when matching
;
; Output Options:
;
;   These options affect how matching lines are printed.  The
;   defaults are ideal for piping into other programs.
;
;   --color           highlight matches with color
;   --no-color        don't highlight matches (the default)
;   -u N, --context N show N lines of context (default 0)

Error Handling

For the most part Adopt doesn't try to be too smart about error handling and leaves it up to you.

However, when Adopt is parsing the command line options it will signal an error of type adopt:unrecognized-option if the user passes a command line option that wasn't defined in the interface:

:::lisp
(defparameter *ui*
  (adopt:make-interface
    :name "meow"
    :summary "say meow"
    :usage "[OPTIONS]"
    :help "Say meow.  Like a cat."
    :contents (list (make-option 'times
                      :parameter "N"
                      :long "times"
                      :initial-value 1
                      :help "Say meow N times (default 1)."
                      :reduce #'adopt:last
                      :key #'parse-integer))))

(adopt:parse-options *ui* '("--times" "5"))
; =>
; NIL
; {TIMES: 5}

(adopt:parse-options *ui* '("--bark"))
; =>
; No such option "--bark".
;    [Condition of type UNRECOGNIZED-OPTION]
;
; Restarts:
;   R 0.  DISCARD-OPTION    - Discard the unrecognized option.
;   R 1.  TREAT-AS-ARGUMENT - Treat the unrecognized option as a plain argument.
;   R 2.  SUPPLY-NEW-VALUE  - Supply a new value to parse.
;   R 3.  RETRY             - Retry SLIME REPL evaluation request.
;   R 4. *ABORT             - Return to SLIME's top level.
;   R 5.  ABORT             - abort thread (#<THREAD "repl-thread" RUNNING {100AF48413}>)

Adopt provides three possible restarts for this condition as seen above. Adopt also provides functions with the same names that invoke the restarts properly, to make it easier to use them programatically with handler-bind. For example:

:::lisp
(handler-bind
    ((adopt:unrecognized-option 'adopt:discard-option))
  (adopt:parse-options *ui* '("--bark")))
; =>
; NIL
; {TIMES: 1}

(handler-bind
    ((adopt:unrecognized-option 'adopt:treat-as-argument))
  (adopt:parse-options *ui* '("--bark")))
; =>
; ("--bark")
; {TIMES: 1}

(handler-bind
    ((adopt:unrecognized-option
       (alexandria:rcurry 'adopt:supply-new-value "--times")))
  (adopt:parse-options *ui* '("--bark" "5")))
; =>
; NIL
; {TIMES: 5}

Generating Man Pages

We've already seen that Adopt can print a pretty help document, but it can also render man pages for you:

:::lisp
(with-open-file (out "man/man1/search.1"
                  :direction :output
                  :if-exists :supersede)
  (adopt:print-manual *ui* :stream out))

The generated man page will contain the same information as the help text by default. If you want to override this (e.g. to provide a short summary of an option in the help text, but elaborate more in the manual), you can use the :manual argument to make-interface and make-option:

:::lisp
(defparameter *option-exclude*
  (adopt:make-option 'exclude
    :long "exclude"
    :parameter "PATTERN"
    :help "Exclude PATTERN."
    :manual "Exclude lines that match PATTERN (a PERL-compatible regular expression) from the search results.  Multiple PATTERNs can be specified by giving this option multiple times."
    :reduce (adopt:flip #'cons)))

In order for man to find the pages, they need to be in the correct place. By default man is usually smart enough to look next to every directory in your $PATH to find a directory called man. So if you put your binaries in /home/me/bin/ you can put your man pages in /home/me/man/ under the appropriate subdirectories and it should all Just Work™. Consult the man documentation for more information.

Implementation Specifics

TODO: talk about Lisp runtime options vs program options.

SBCL

You'll want to use :save-runtime-options t in the call to sb-ext:save-lisp-and-die.

ClozureCL

See https://github.com/Clozure/ccl/issues/177.