--- /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
--- 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)
--- 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)'
--- 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
--- 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)
--- 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>.
--- 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)
--- 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.
+
+
--- 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
--- 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)
--- 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
--- 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")))))