d70dd2b9e3c2

Merge.
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Mon, 01 Jul 2019 16:03:23 -0400
parents bc8760190ed7 (current diff) 8115d14339b3 (diff)
children 6efdac6231f2 d5dbc46f6ddb
branches/tags (none)
files adopt.asd src/main.lisp

Changes

diff -r bc8760190ed7 -r d70dd2b9e3c2 .hgtags
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Mon Jul 01 16:03:23 2019 -0400
@@ -0,0 +1,1 @@
+1a81296fb3ecaf2871b0170a65dc6224a9da6bc5 v1.0.0
diff -r bc8760190ed7 -r d70dd2b9e3c2 .lispwords
--- a/.lispwords	Mon Jul 01 16:02:40 2019 -0400
+++ b/.lispwords	Mon Jul 01 16:03:23 2019 -0400
@@ -1,1 +1,2 @@
 (1 make-option)
+(1 signals)
diff -r bc8760190ed7 -r d70dd2b9e3c2 Makefile
--- a/Makefile	Mon Jul 01 16:02:40 2019 -0400
+++ b/Makefile	Mon Jul 01 16:03:23 2019 -0400
@@ -1,4 +1,4 @@
-.PHONY: test test-sbcl test-ccl test-ecl test-abcl pubdocs
+.PHONY: test test-sbcl test-ccl test-ecl test-abcl test-clasp pubdocs
 
 heading_printer = $(shell which heading || echo 'true')
 sourcefiles = $(shell ffind --full-path --literal .lisp)
@@ -6,7 +6,7 @@
 apidocs = $(shell ls docs/*reference*.markdown)
 
 # Testing ---------------------------------------------------------------------
-test: test-sbcl test-ccl test-ecl test-abcl
+test: test-sbcl test-ccl test-ecl test-abcl test-clasp
 
 test-sbcl:
 	$(heading_printer) computer 'SBCL'
@@ -24,6 +24,10 @@
 	$(heading_printer) broadway 'ABCL'
 	time abcl --load test/run.lisp
 
+test-clasp:
+	$(heading_printer) o8 'CLASP'
+	time clasp --load test/run.lisp
+
 # Documentation ---------------------------------------------------------------
 $(apidocs): $(sourcefiles)
 	sbcl --noinform --load docs/api.lisp  --eval '(quit)'
diff -r bc8760190ed7 -r d70dd2b9e3c2 README.markdown
--- a/README.markdown	Mon Jul 01 16:02:40 2019 -0400
+++ b/README.markdown	Mon Jul 01 16:03:23 2019 -0400
@@ -4,13 +4,27 @@
 I needed **a** **d**amn **opt**ion parsing library.
 
 Adopt is a simple UNIX-style option parser in Common Lisp, heavily influenced by
-Python's optparse and argparse.  It depends on [Bobbin][] and
-[split-sequence][].
+Python's optparse and argparse.
 
 * **License:** MIT
 * **Documentation:** <https://sjl.bitbucket.io/adopt/>
 * **Mercurial:** <https://bitbucket.org/sjl/adopt/>
 * **Git:** <https://github.com/sjl/adopt/>
 
+Adopt aims to be a simple, robust option parser.  It can automatically print
+help information and even generate `man` pages for you.
+
+Adopt is compatible with Quicklisp, but not *in* Quicklisp (yet).  You can clone
+the repository into your [Quicklisp local-projects directory][local] for now.
+
+The `adopt` system contains the core API and depends on [Bobbin][] and
+[split-sequence][].
+
+The `adopt/test` system contains the test suite, which depends on some other
+systems.  You don't need to load this unless you want to run the unit tests.
+The tests pass on SBCL, CCL, ECL, and ABCL on Ubuntu 18.04.  Further testing is
+welcome.
+
+[local]: https://www.quicklisp.org/beta/faq.html#local-project
 [Bobbin]: https://github.com/sjl/bobbin
 [split-sequence]: https://www.cliki.net/split-sequence
diff -r bc8760190ed7 -r d70dd2b9e3c2 adopt.asd
--- a/adopt.asd	Mon Jul 01 16:02:40 2019 -0400
+++ b/adopt.asd	Mon Jul 01 16:03:23 2019 -0400
@@ -4,7 +4,7 @@
   :homepage "https://sjl.bitbucket.io/adopt/"
 
   :license "MIT"
-  :version "0.0.1"
+  :version "1.0.0"
 
   :depends-on (:bobbin :split-sequence)
 
diff -r bc8760190ed7 -r d70dd2b9e3c2 docs/01-usage.markdown
--- a/docs/01-usage.markdown	Mon Jul 01 16:02:40 2019 -0400
+++ b/docs/01-usage.markdown	Mon Jul 01 16:03:23 2019 -0400
@@ -1,8 +1,8 @@
 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.
+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.
 
 [TOC]
 
@@ -16,9 +16,9 @@
 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.
+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
 
@@ -26,6 +26,7 @@
 the world certainly needs *another* `grep` replacement).  You might start with
 something like:
 
+    :::lisp
     (defparameter *ui*
       (adopt:make-interface
         :name "search"
@@ -33,25 +34,38 @@
         :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`:
+`make-interface` takes several required arguments:
 
+* `:name` is the name of the program.
+* `:summary` is a concise one-line summary of what it does.
+* `:usage` is a UNIX-style the command line usage string.
+* `:help` is a longer description of the program.
+
+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: … [OPTIONS] PATTERN [FILE]...
-
-    Search the contents of each FILE for the regular expression PATTERN.  If no
-    files are specified, searches standard input instead.
+    ; 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 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:
+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][tilde-newline]) to
+preprocess the help text argument:
+
+[tilde-newline]: http://www.lispworks.com/documentation/lw71/CLHS/Body/22_cic.htm
+
+    :::lisp
     (defparameter *ui*
       (adopt:make-interface
         :name "search"
@@ -62,52 +76,94 @@
                            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:
+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 (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.")))
+        :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:
-
-    (adopt:print-help *ui* :width 50)
-    ; =>
-    search - search files for a regular expression
-
-    USAGE: … [OPTIONS] PATTERN [FILE]...
+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:
 
-    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.
+[Bobbin]: https://sjl.bitbucket.io/bobbin/
 
-`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:
+    :::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 *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")
@@ -118,31 +174,31 @@
 
     (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 -
+    ; 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
@@ -155,9 +211,10 @@
 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:exit 1)
 
     (adopt:print-help-and-exit *ui*)
 
@@ -171,7 +228,7 @@
 
 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.
+yourself if you prefer.
 
 Options
 -------
@@ -179,6 +236,7 @@
 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"
@@ -212,29 +270,31 @@
 
 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 …
+    ; 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
 
-    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.
+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.
 
 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
+`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

@@ -247,38 +307,84 @@
 -------
 
 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:
+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}>
-
-`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.
+    ; ("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}
+    ; ("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 create with Adopt:
+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*)
+          (multiple-value-bind (arguments options) (adopt:parse-options *ui*)
             (when (gethash 'help options)
               (adopt:print-help-and-exit *ui*))
             (when (gethash 'version options)
@@ -291,25 +397,552 @@
         (error (c)
           (adopt:print-error-and-exit c))))
 
-    (sb-ext:save-lisp-and-die "search" :toplevel #'toplevel)
+    (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:
 
-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.
+* The `toplevel` function takes care of parsing arguments and exiting with an
+  appropriate status code if necessary.
+* The `run` function takes parsed, Lispy arguments and actually *does*
+  something.  When developing (in SLIME, VLIME, etc) you'll call `run`, because
+  you don't want the program to exit when you're developing interactively.
+* The `build` function dumps an executable binary.  For more complicated
+  programs you might use something fancier, like ASDF or Shinmera's Deploy
+  library instead.
 
-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.
+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.
 
-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.
+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:
+
+* Simple options that are either given or not, like `--help` or `--version`.
+* Boolean options, like git's `-p/--paginate` and `--no-pager`, where both options affect a single boolean flag.
+* Counted options, where the number of times they are given has an effect, like SSH's `-v` option (more `-v`'s means more verbosity).
+* Options that take a single parameter, like Mercurial's `--repository /path/to/repo` option, which specifies the path to a repository to work on.
+* Options that collect all parameters they are given, like rsync's `--exclude PATTERN`, which you can pass multiple times to add several exclusions.
+
+An option-parsing library needs to give you the tools to handle all of these
+cases (and more).  Python's [argparse][ap-actions] 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.
+
+[ap-actions]: https://docs.python.org/3/library/argparse.html#action
+[reduce]: http://www.lispworks.com/documentation/HyperSpec/Body/f_reduce.htm
+
+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 information 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`](http://www.lispworks.com/documentation/HyperSpec/Body/f_cons_1.htm)
+function, you can do this more concisely:
+
+    :::lisp
+    (defparameter *option-help*
+      (adopt:make-option 'help
+        :long "help"
+        :short #\h
+        :help "display help information 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.
+
+### 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](https://en.wikibooks.org/wiki/Haskell/Higher-order_functions#Flipping_arguments),
+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>.
diff -r bc8760190ed7 -r d70dd2b9e3c2 docs/02-reference.markdown
--- a/docs/02-reference.markdown	Mon Jul 01 16:02:40 2019 -0400
+++ b/docs/02-reference.markdown	Mon Jul 01 16:03:23 2019 -0400
@@ -18,9 +18,9 @@
 
 Return a list of the program name and command line arguments.
 
-  This is not implemented for every Common Lisp implementation.  You can always
-  pass your own values to `parse-options` and `print-help` if it's not
-  implemented for your particular Lisp.
+  This is not implemented for every Common Lisp implementation.  You can pass
+  your own values to `parse-options` and `print-help` if it's not implemented
+  for your particular Lisp.
 
   
 
@@ -62,6 +62,14 @@
 
     (EXIT &OPTIONAL (CODE 0))
 
+Exit the program with status `code`.
+
+  This is not implemented for every Common Lisp implementation.  You can write
+  your own version of it and pass it to `print-help-and-exit` and
+  `print-error-and-exit` if it's not implemented for your particular Lisp.
+
+  
+
 ### `FIRST` (function)
 
     (FIRST OLD NEW)
@@ -106,14 +114,74 @@
 
     (MAKE-GROUP NAME &KEY TITLE HELP MANUAL OPTIONS)
 
+Create and return an option group, suitable for use in an interface.
+
+  This function takes a number of arguments that define how the group is
+  presented to the user:
+
+  * `name` (**required**): a symbol naming the group.
+  * `title` (optional): a title for the group for use in the help text.
+  * `help` (optional): a short summary of this group of options for use in the help text.
+  * `manual` (optional): used in place of `help` when rendering a man page.
+  * `options` (**required**): the options to include in the group.
+
+  See the full documentation for more information.
+
+  
+
 ### `MAKE-INTERFACE` (function)
 
     (MAKE-INTERFACE &KEY NAME SUMMARY USAGE HELP MANUAL EXAMPLES CONTENTS)
 
+Create and return a command line interface.
+
+  This function takes a number of arguments that define how the interface is
+  presented to the user:
+
+  * `name` (**required**): a symbol naming the interface.
+  * `summary` (**required**): a string of a concise, one-line summary of what the program does.
+  * `usage` (**required**): a string of a UNIX-style usage summary, e.g. `"[OPTIONS] PATTERN [FILE...]"`.
+  * `help` (**required**): a string of a longer description of the program.
+  * `manual` (optional): a string to use in place of `help` when rendering a man page.
+  * `examples` (optional): an alist of `(prose . command)` conses to render as a list of examples.
+  * `contents` (optional): a list of options and groups.  Ungrouped options will be collected into a single top-level group.
+
+  See the full documentation for more information.
+
+  
+
 ### `MAKE-OPTION` (function)
 
-    (MAKE-OPTION NAME &KEY LONG SHORT HELP MANUAL PARAMETER REDUCE (RESULT-KEY NAME)
-                 (INITIAL-VALUE NIL INITIAL-VALUE?) (KEY #'IDENTITY) (FINALLY #'IDENTITY))
+    (MAKE-OPTION NAME &KEY LONG SHORT HELP MANUAL PARAMETER REDUCE
+                 (INITIAL-VALUE NIL INITIAL-VALUE?) (RESULT-KEY NAME) (KEY #'IDENTITY)
+                 (FINALLY #'IDENTITY))
+
+Create and return an option, suitable for use in an interface.
+
+  This function takes a number of arguments, some required, that define how the
+  option interacts with the user.
+
+  * `name` (**required**): a symbol naming the option.
+  * `help` (**required**): a short string describing what the option does.
+  * `result-key` (optional): a symbol to use as the key for this option in the hash table of results.
+  * `long` (optional): a string for the long form of the option (e.g. `--foo`).
+  * `short` (optional): a character for the short form of the option (e.g. `-f`).  At least one of `short` and `long` must be given.
+  * `manual` (optional): a string to use in place of `help` when rendering a man page.
+  * `parameter` (optional): a string.  If given, it will turn this option into a parameter-taking option (e.g. `--foo=bar`) and will be used as a placeholder
+  in the help text.
+  * `reduce` (**required**): a function designator that will be called every time the option is specified by the user.
+  * `initial-value` (optional): a value to use as the initial value of the option.
+  * `key` (optional): a function designator, only allowed for parameter-taking options, to be called on the values given by the user before they are passed along to the reducing function.  It will not be called on the initial value.
+  * `finally` (optional): a function designator to be called on the final result after all parsing is complete. 
+
+  The manner in which the reducer is called depends on whether the option takes a parameter:
+
+  * For options that don't take parameters, it will be called with the old value.
+  * For options that take parameters, it will be called with the old value and the value given by the user.
+
+  See the full documentation for more information.
+
+  
 
 ### `PARSE-OPTIONS` (function)
 
@@ -133,7 +201,8 @@
 
 ### `PRINT-ERROR-AND-EXIT` (function)
 
-    (PRINT-ERROR-AND-EXIT ERROR &KEY (STREAM *ERROR-OUTPUT*) (EXIT-CODE 1) (PREFIX error: ))
+    (PRINT-ERROR-AND-EXIT ERROR &KEY (STREAM *ERROR-OUTPUT*) (EXIT-FUNCTION #'EXIT) (EXIT-CODE 1)
+                          (PREFIX error: ))
 
 Print `prefix` and `error` to `stream` and exit.
 
@@ -192,7 +261,8 @@
 ### `PRINT-HELP-AND-EXIT` (function)
 
     (PRINT-HELP-AND-EXIT INTERFACE &KEY (STREAM *STANDARD-OUTPUT*) (PROGRAM-NAME (CAR (ARGV)))
-                         (WIDTH 80) (OPTION-WIDTH 20) (INCLUDE-EXAMPLES T) (EXIT-CODE 0))
+                         (WIDTH 80) (OPTION-WIDTH 20) (INCLUDE-EXAMPLES T) (EXIT-FUNCTION #'EXIT)
+                         (EXIT-CODE 0))
 
 Print a pretty help document for `interface` to `stream` and exit.
 
@@ -202,12 +272,24 @@
       (when (gethash 'help options)
         (print-help-and-exit *ui*))
       (run arguments options))
+
   
 
 ### `PRINT-MANUAL` (function)
 
     (PRINT-MANUAL INTERFACE &KEY (STREAM *STANDARD-OUTPUT*) (MANUAL-SECTION 1))
 
+Print a troff-formatted man page for `interface` to `stream`.
+
+  Example:
+
+    (with-open-file (manual "man/man1/foo.1"
+                            :direction :output
+                            :if-exists :supersede)
+      (print-manual *ui* manual))
+
+  
+
 ### `SUPPLY-NEW-VALUE` (function)
 
     (SUPPLY-NEW-VALUE CONDITION VALUE)
diff -r bc8760190ed7 -r d70dd2b9e3c2 docs/03-changelog.markdown
--- a/docs/03-changelog.markdown	Mon Jul 01 16:02:40 2019 -0400
+++ b/docs/03-changelog.markdown	Mon Jul 01 16:03:23 2019 -0400
@@ -5,7 +5,9 @@
 
 [TOC]
 
-Current Development Version
----------------------------
+1.0.0
+-----
 
-Adopt is still in active development.  No promises yet.
+Initial release.
+
+
diff -r bc8760190ed7 -r d70dd2b9e3c2 docs/index.markdown
--- a/docs/index.markdown	Mon Jul 01 16:02:40 2019 -0400
+++ b/docs/index.markdown	Mon Jul 01 16:03:23 2019 -0400
@@ -11,9 +11,6 @@
 Adopt aims to be a simple, robust option parser.  It can automatically print
 help information and even generate `man` pages for you.
 
-The test suite currently passes in SBCL, CCL, ECL, and ABCL on Ubuntu.  Further
-testing is welcome.
-
 Adopt is compatible with Quicklisp, but not *in* Quicklisp (yet).  You can clone
 the repository into your [Quicklisp local-projects directory][local] for now.
 
@@ -22,6 +19,8 @@
 
 The `adopt/test` system contains the test suite, which depends on some other
 systems.  You don't need to load this unless you want to run the unit tests.
+The tests pass on SBCL, CCL, ECL, and ABCL on Ubuntu 18.04.  Further testing is
+welcome.
 
 [local]: https://www.quicklisp.org/beta/faq.html#local-project
 [Bobbin]: https://github.com/sjl/bobbin
diff -r bc8760190ed7 -r d70dd2b9e3c2 src/main.lisp
--- a/src/main.lisp	Mon Jul 01 16:02:40 2019 -0400
+++ b/src/main.lisp	Mon Jul 01 16:03:23 2019 -0400
@@ -55,9 +55,9 @@
 (defun argv ()
   "Return a list of the program name and command line arguments.
 
-  This is not implemented for every Common Lisp implementation.  You can always
-  pass your own values to `parse-options` and `print-help` if it's not
-  implemented for your particular Lisp.
+  This is not implemented for every Common Lisp implementation.  You can pass
+  your own values to `parse-options` and `print-help` if it's not implemented
+  for your particular Lisp.
 
   "
   #+sbcl sb-ext:*posix-argv*
@@ -68,6 +68,13 @@
   #-(or sbcl ccl ecl) (error "ARGV is not supported on this implementation."))
 
 (defun exit (&optional (code 0))
+  "Exit the program with status `code`.
+
+  This is not implemented for every Common Lisp implementation.  You can write
+  your own version of it and pass it to `print-help-and-exit` and
+  `print-error-and-exit` if it's not implemented for your particular Lisp.
+
+  "
   #+sbcl (sb-ext:exit :code code)
   #+ccl (ccl:quit code)
   #-(or sbcl ccl) (error "EXIT is not supported on this implementation."))
@@ -91,6 +98,12 @@
             :collect `(,slot :initarg ,initarg :accessor ,slot))))
 
 
+(defmacro check-types (&rest place-type-pairs)
+  `(progn
+     ,@(loop :for (place type) :on place-type-pairs :by #'cddr
+             :collect `(check-type ,place ,type))))
+
+
 ;;;; Definition ---------------------------------------------------------------
 (defclass* option
   name
@@ -116,10 +129,38 @@
                     manual
                     parameter
                     reduce
+                    ;; can't just default to nil because multiple options might
+                    ;; have the same result key, and only one can provide init
+                    (initial-value nil initial-value?)
                     (result-key name)
-                    (initial-value nil initial-value?)
                     (key #'identity)
                     (finally #'identity))
+  "Create and return an option, suitable for use in an interface.
+
+  This function takes a number of arguments, some required, that define how the
+  option interacts with the user.
+
+  * `name` (**required**): a symbol naming the option.
+  * `help` (**required**): a short string describing what the option does.
+  * `result-key` (optional): a symbol to use as the key for this option in the hash table of results.
+  * `long` (optional): a string for the long form of the option (e.g. `--foo`).
+  * `short` (optional): a character for the short form of the option (e.g. `-f`).  At least one of `short` and `long` must be given.
+  * `manual` (optional): a string to use in place of `help` when rendering a man page.
+  * `parameter` (optional): a string.  If given, it will turn this option into a parameter-taking option (e.g. `--foo=bar`) and will be used as a placeholder
+  in the help text.
+  * `reduce` (**required**): a function designator that will be called every time the option is specified by the user.
+  * `initial-value` (optional): a value to use as the initial value of the option.
+  * `key` (optional): a function designator, only allowed for parameter-taking options, to be called on the values given by the user before they are passed along to the reducing function.  It will not be called on the initial value.
+  * `finally` (optional): a function designator to be called on the final result after all parsing is complete. 
+
+  The manner in which the reducer is called depends on whether the option takes a parameter:
+
+  * For options that don't take parameters, it will be called with the old value.
+  * For options that take parameters, it will be called with the old value and the value given by the user.
+
+  See the full documentation for more information.
+
+  "
   (when (and (null long) (null short))
     (error "Option ~A requires at least one of :long/:short." name))
   (when (null reduce)
@@ -132,19 +173,24 @@
              (null parameter))
     (error "Option ~A has reduce function ~A, which requires a :parameter."
            name reduce))
+  (check-types short (or null character)
+               long (or null string)
+               help string
+               manual (or null string)
+               parameter (or null string))
   (apply #'make-instance 'option
-         :name name
-         :result-key result-key
-         :help help
-         :manual manual
-         :long long
-         :short short
-         :parameter parameter
-         :reduce reduce
-         :key key
-         :finally finally
-         (when initial-value?
-           (list :initial-value initial-value))))
+    :name name
+    :result-key result-key
+    :help help
+    :manual manual
+    :long long
+    :short short
+    :parameter parameter
+    :reduce reduce
+    :key key
+    :finally finally
+    (when initial-value?
+      (list :initial-value initial-value))))
 
 (defun optionp (object)
   (typep object 'option))
@@ -158,6 +204,28 @@
     (format stream "~A (~D options)" (name g) (length (options g)))))
 
 (defun make-group (name &key title help manual options)
+  "Create and return an option group, suitable for use in an interface.
+
+  This function takes a number of arguments that define how the group is
+  presented to the user:
+
+  * `name` (**required**): a symbol naming the group.
+  * `title` (optional): a title for the group for use in the help text.
+  * `help` (optional): a short summary of this group of options for use in the help text.
+  * `manual` (optional): used in place of `help` when rendering a man page.
+  * `options` (**required**): the options to include in the group.
+
+  See the full documentation for more information.
+
+  "
+  (check-types name symbol
+               title (or null string)
+               help (or null string)
+               manual (or null string)
+               options list)
+  (assert (every #'optionp options) (options)
+    "The :options argument to ~S was not a list of options.  Got: ~S"
+    'make-group options)
   (make-instance 'group
     :name name
     :title title
@@ -197,6 +265,29 @@
             (length (groups i)))))
 
 (defun make-interface (&key name summary usage help manual examples contents)
+  "Create and return a command line interface.
+
+  This function takes a number of arguments that define how the interface is
+  presented to the user:
+
+  * `name` (**required**): a symbol naming the interface.
+  * `summary` (**required**): a string of a concise, one-line summary of what the program does.
+  * `usage` (**required**): a string of a UNIX-style usage summary, e.g. `\"[OPTIONS] PATTERN [FILE...]\"`.
+  * `help` (**required**): a string of a longer description of the program.
+  * `manual` (optional): a string to use in place of `help` when rendering a man page.
+  * `examples` (optional): an alist of `(prose . command)` conses to render as a list of examples.
+  * `contents` (optional): a list of options and groups.  Ungrouped options will be collected into a single top-level group.
+
+  See the full documentation for more information.
+
+  "
+  (check-types name string
+               summary string
+               usage string
+               help string
+               manual (or null string)
+               examples list
+               contents list)
   (let* ((ungrouped-options (remove-if-not #'optionp contents))
          (groups (cons (make-default-group ungrouped-options)
                        (remove-if-not #'groupp contents)))
@@ -216,11 +307,16 @@
              (let ((short (short option))
                    (long (long option)))
                (when short
+                 (when (gethash short (short-options interface))
+                   (error "Duplicate short option ~S." short))
                  (setf (gethash short (short-options interface)) option))
                (when long
+                 (when (gethash long (long-options interface))
+                   (error "Duplicate long option ~S." long))
                  (setf (gethash long (long-options interface)) option)))))
       (dolist (g groups)
         (map nil #'add-option (options g))))
+    ;; TODO: check for multiple conflicting initial-values
     interface))
 
 
@@ -414,7 +510,7 @@
         (col 0))
     (flet ((print-at (c string &optional newline)
              "Print `string` starting at column `c`, adding padding/newline if needed."
-             (when (> col c)
+             (when (>= col c)
                (terpri stream)
                (setf col 0))
              (format stream "~vA~A" (- c col) #\space string)
@@ -478,10 +574,16 @@
   (dolist (group (groups interface))
     (when (options group)
       (format stream "~%~A:~%" (or (title group) (name group) "Options"))
-      (let* ((option-column 2)
+      (let* ((help (help group))
+             (help-column 2)
+             (help-width (- width help-column))
+             (option-column 2)
              (option-padding 2)
              (doc-column (+ option-column option-width option-padding))
              (doc-width (- width doc-column)))
+        (when help
+          (format stream "~%~{  ~A~^~%~}~2%"
+                  (bobbin:wrap (list help) help-width)))
         (dolist (option (options group))
           (print-option-help stream option option-column doc-column doc-width)))))
   (let* ((examples (examples interface))
@@ -501,6 +603,7 @@
      (width 80)
      (option-width 20)
      (include-examples t)
+     (exit-function #'exit)
      (exit-code 0))
   "Print a pretty help document for `interface` to `stream` and exit.
 
@@ -510,17 +613,19 @@
       (when (gethash 'help options)
         (print-help-and-exit *ui*))
       (run arguments options))
+
   "
   (print-help interface
-               :stream stream
-               :program-name program-name
-               :width width
-               :option-width option-width
-               :include-examples include-examples)
-  (exit exit-code))
+              :stream stream
+              :program-name program-name
+              :width width
+              :option-width option-width
+              :include-examples include-examples)
+  (funcall exit-function exit-code))
 
 (defun print-error-and-exit (error &key
                              (stream *error-output*)
+                             (exit-function #'exit)
                              (exit-code 1)
                              (prefix "error: "))
   "Print `prefix` and `error` to `stream` and exit.
@@ -535,7 +640,7 @@
 
   "
   (format stream "~A~A~%" (or prefix "") error)
-  (exit exit-code))
+  (funcall exit-function exit-code))
 
 
 ;;;; Man ----------------------------------------------------------------------
@@ -579,6 +684,16 @@
 (defun print-manual (interface &key
                      (stream *standard-output*)
                      (manual-section 1))
+  "Print a troff-formatted man page for `interface` to `stream`.
+
+  Example:
+
+    (with-open-file (manual \"man/man1/foo.1\"
+                            :direction :output
+                            :if-exists :supersede)
+      (print-manual *ui* manual))
+
+  "
   (check-type manual-section (integer 1))
   (labels
       ((f (&rest args)
diff -r bc8760190ed7 -r d70dd2b9e3c2 src/package.lisp
--- a/src/package.lisp	Mon Jul 01 16:02:40 2019 -0400
+++ b/src/package.lisp	Mon Jul 01 16:03:23 2019 -0400
@@ -24,7 +24,6 @@
     :supply-new-value
 
     :flip
-    :oldest
     :collect
     :first
     :last
diff -r bc8760190ed7 -r d70dd2b9e3c2 test/tests.lisp
--- a/test/tests.lisp	Mon Jul 01 16:02:40 2019 -0400
+++ b/test/tests.lisp	Mon Jul 01 16:03:23 2019 -0400
@@ -38,31 +38,45 @@
        (is (equal ,expected-args ,args))
        (is (hash-table-equal ,expected-result ,result)))))
 
+(defun ct () (constantly t))
+
 
 ;;;; Tests --------------------------------------------------------------------
 (defparameter *noop*
-  (adopt:make-interface))
+  (adopt:make-interface
+    :name "noop"
+    :summary "no options"
+    :help "this interface has no options"
+    :usage ""))
 
 (defparameter *option-types*
   (adopt:make-interface
+    :name "option-types"
+    :summary "testing option types"
+    :help "this interface tests both option types"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option 'long
         :help "long only"
         :long "long"
-        :reduce (constantly t))
+        :reduce (ct))
       (adopt:make-option 'short
         :help "short only"
         :short #\s
-        :reduce (constantly t))
+        :reduce (ct))
       (adopt:make-option 'both
         :help "both short and long"
         :short #\b
         :long "both"
-        :reduce (constantly t)))))
+        :reduce (ct)))))
 
 (defparameter *reducers*
   (adopt:make-interface
+    :name "reducers"
+    :summary "testing reducers"
+    :help "this interface tests basic reducers"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option 'c1
@@ -94,6 +108,10 @@
 
 (defparameter *same-key*
   (adopt:make-interface
+    :name "same-key"
+    :summary "testing same keys"
+    :help "this interface tests options with the same result-key"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option '1
@@ -109,6 +127,10 @@
 
 (defparameter *initial-value*
   (adopt:make-interface
+    :name "initial-value"
+    :summary "testing initial values"
+    :help "this interface tests the initial-value argument"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option 'foo
@@ -120,6 +142,10 @@
 
 (defparameter *finally*
   (adopt:make-interface
+    :name "finally"
+    :summary "testing finally"
+    :help "this interface tests the finally argument"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option 'yell
@@ -140,8 +166,12 @@
                    (assert (string= "a" a))
                    :ok)))))
 
-(defparameter *keys*
+(defparameter *key*
   (adopt:make-interface
+    :name "key"
+    :summary "testing key"
+    :help "this interface tests the key argument"
+    :usage "[OPTIONS]"
     :contents
     (list
       (adopt:make-option 'int
@@ -196,7 +226,12 @@
          '("foo" "bar")
          (result 'short t
                  'long t
-                 'both t)))
+                 'both t))
+  ;; Make sure we require at least one of short/long.
+  (is
+    (adopt:make-option 'foo :reduce (ct) :help "this should work" :short #\x))
+  (signals error
+    (adopt:make-option 'foo :reduce (ct) :help "this should not work")))
 
 (define-test reducers
   (check *reducers* ""
@@ -248,12 +283,12 @@
          '("x" "y")
          (result 'foo "goodbye")))
 
-(define-test keys
-  (check *keys* ""
+(define-test key
+  (check *key* ""
          '()
          (result 'len '()
                  'int '()))
-  (check *keys* "--int 123 --int 0 --len abc --len 123456"
+  (check *key* "--int 123 --int 0 --len abc --len 123456"
          '()
          (result 'int '(123 0)
                  'len '(3 6))))
@@ -272,3 +307,20 @@
   (is (equal '(:a) (adopt:collect '() :a)))
   (is (equal '(:a :b) (adopt:collect '(:a) :b)))
   (is (equal '(2 . 1) (funcall (adopt:flip 'cons) 1 2))))
+
+(define-test duplicate-options
+  (is
+    (adopt:make-interface
+      :name "" :summary "" :help "" :usage "" :contents
+      (list (adopt:make-option 'foo :reduce (ct) :help "" :short #\a :long "foo")
+            (adopt:make-option 'bar :reduce (ct) :help "" :short #\b :long "bar"))))
+  (signals error
+    (adopt:make-interface
+      :name "" :summary "" :help "" :usage "" :contents
+      (list (adopt:make-option 'foo :reduce (ct) :help "" :short #\a :long "foo")
+            (adopt:make-option 'bar :reduce (ct) :help "" :short #\a :long "bar"))))
+  (signals error
+    (adopt:make-interface
+      :name "" :summary "" :help "" :usage "" :contents
+      (list (adopt:make-option 'foo :reduce (ct) :help "" :short #\a :long "oops")
+            (adopt:make-option 'bar :reduce (ct) :help "" :short #\b :long "oops")))))