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.
All core Adopt functions are in the adopt
package. Several of the symbols in
adopt shadow those in the common-lisp
package, so you should probably use
namespaced adopt:…
symbols instead of USE
ing the package.
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.
Let's say you're developing a program to search the contents of files (because
the world certainly needs another grep
replacement). You might start with
something like:
:::lisp
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help "Search the contents of each FILE for the regular expression PATTERN. If no files are specified, searches standard input instead."))
make-interface
takes several required arguments:
:name
is the name of the program.:summary
is a concise one-line summary of what it does. By convention it
should not be a full sentence, but just a snippet of text.: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: /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.
Adopt will handle line-wrapping your help text, so you don't need to (and shouldn't) add extra line breaks when creating your interface.
If you want to line break the text in your source code to fit nicely in your
text editor, remember that adopt:make-interface
is just a function — you can
use format
(possibly with its ~Newline
directive) to
preprocess the help text argument:
:::lisp
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help (format nil "Search the contents of each FILE for ~
the regular expression PATTERN. If ~
no files are specified, searches ~
standard input instead.")))
If you want to pull out the documentation string into its own variable to keep
that make-interface
call from getting too unwieldy, you can certainly do that:
:::lisp
(defparameter *help-text*
(format nil "Search the contents of each FILE for the ~
regular expression PATTERN. If no files ~
are specified, searches standard input ~
instead."))
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help *help-text*))
The (defparameter … (format nil …))
pattern can be tedious to write, so Adopt
provides a helper macro define-string
that does exactly that:
:::lisp
(adopt:define-string *help-text*
"Search the contents of each FILE for the regular ~
expression PATTERN. If no files are specified, ~
searches standard input instead.")
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help *help-text*))
Adopt's line-wrapping library Bobbin will only ever add line breaks, never
remove them, which means you can include breaks in the output if you want to
have multiple paragraphs in your help text. Once again, format
is your
friend:
:::lisp
(adopt:define-string *help-text*
"Search the contents of each FILE for the regular ~
expression PATTERN.~@
~@
If no files are specified (or if - is given as a ~
file name), standard input will be searched instead.")
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help *help-text*))
If you want to control the width of the help text lines when they are printed,
adopt:print-help
takes a :width
argument:
:::lisp
(adopt:print-help *ui* :width 50)
; =>
; search - search files for a regular expression
;
; USAGE: … [OPTIONS] PATTERN [FILE]...
;
; Search the contents of each FILE for the regular
; expression PATTERN.
;
; If no files are specified (or if - is given as a
; file name), standard input will be searched
; instead.
adopt:print-help
takes a number of other options — see the API Reference for
more information.
Describing the CLI in detail is helpful, but users can often learn a lot more by
seeing a few examples of its usage. make-interface
can take an :examples
argument, which should be an alist of (description . example)
conses:
:::lisp
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help *help-text*
:examples
'(("Search foo.txt for the string 'hello':"
. "search hello foo.txt")
("Search standard input for lines starting with x:"
. "search '^x' -")
("Watch the file log.txt for lines containing the username steve.losh:"
. "tail foo/bar/baz/log.txt | search --literal steve.losh -"))))
(adopt:print-help *ui* :width 50)
; =>
; search - search files for a regular expression
;
; USAGE: … [OPTIONS] PATTERN [FILE]...
;
; Search the contents of each FILE for the regular
; expression PATTERN.
;
; If no files are specified (or if - is given as a
; file name) standard input will be searched
; instead.
;
; Examples:
;
; Search foo.txt for the string 'hello':
;
; search hello foo.txt
;
; Search standard input for lines starting with x:
;
; search '^x' -
;
; Watch the file log.txt for lines containing the
; username steve.losh:
;
; tail foo/bar/baz/log.txt | search --literal steve.losh -
Notice how Adopt line wraps the prose explaining each example, but leaves the example itself untouched for easier copying and pasting. In general Adopt tries to do the right thing for your users (even when that means making a little more work for you in certain places).
Adopt provides some helpful utility functions to exit out of your program with a UNIX exit code. These do what you think they do:
:::lisp
(adopt:exit)
(adopt:exit 1)
(adopt:print-help-and-exit *ui*)
(adopt:print-help-and-exit *ui*
:stream *error-output*
:exit-code 1)
(handler-case (assert (= 1 0))
(error (err)
(adopt:print-error-and-exit err)))
These functions are not implemented for every Lisp implementation. PRs are welcome, or you can just write the implementation-specific calls in your program yourself if you prefer.
Now that you know how to create an interface, you can create some options to use
inside it with adopt:make-option
:
:::lisp
(defparameter *option-version*
(adopt:make-option 'version
:long "version"
:help "Display version and exit."
:reduce (constantly t)))
(defparameter *option-help*
(adopt:make-option 'help
:long "help"
:short #\h
:help "Display help and exit."
:reduce (constantly t)))
(defparameter *option-literal*
(adopt:make-option 'literal
:long "literal"
:short #\l
:help "Treat PATTERN as a literal string instead of a regular expression."
:reduce (constantly t)))
(defparameter *ui*
(adopt:make-interface
:name "search"
:summary "search files for a regular expression"
:usage "[OPTIONS] PATTERN [FILE]..."
:help "Search the contents of …"
:contents (list
*option-version*
*option-help*
*option-literal*)))
Adopt will automatically add the options to the help text:
:::lisp
(adopt:print-help *ui*)
; =>
; search - search files for a regular expression
;
; USAGE: /usr/local/bin/sbcl [OPTIONS] PATTERN [FILE]...
;
; Search the contents of …
;
; Options:
; --version Display version and exit.
; -h, --help Display help and exit.
; -l, --literal Treat PATTERN as a literal string instead of a regular
; expression.
The first argument to make-option
is the name of the option, which we'll see
put to use shortly. At least one of :short
and :long
is required, and
:help
text must be specified. We'll talk more about :reduce
in a little
while, but it too is required.
When writing :help
text I recommend using a full sentence, starting with
a capital letter and ending with appropriate punctuation. If there's a default
value or behavior for the option, mention it in the help text with something
like Search at most N lines (default 100).
.
I prefer to define each option as its own global variable to keep the call to
make-interface
from getting too large and unwieldy, but feel free to do
something like this if you prefer to avoid cluttering your package:
:::lisp
(defparameter *ui*
(adopt:make-interface
…
:contents
(list (adopt:make-option 'foo …)
(adopt:make-option 'bar …)
…)))
At this point we've got an interface with some options, so we can use it to
parse a list of strings we've received as command line arguments with
adopt:parse-options
:
:::lisp
(adopt:parse-options *ui* '("foo.*" "--literal" "a.txt" "b.txt"))
; =>
; ("foo.*" "a.txt" "b.txt")
; #<HASH-TABLE :TEST EQL :COUNT 3 {10103142A3}>
From now on I'll use a special pretty printer for hash tables to make it easier to see what's inside them:
:::lisp
(adopt:parse-options *ui* '("foo.*" "--literal" "a.txt" "b.txt"))
; =>
; ("foo.*" "a.txt" "b.txt")
; {LITERAL: T, VERSION: NIL, HELP: NIL}
parse-options
returns two values:
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).
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.
We'll look at how the option values are computed shortly, but first let's see the overall structure of the programs you'll typically create with Adopt:
:::lisp
(defun run (pattern files &key literal)
;; Actually do something here.
)
(defun toplevel ()
(handler-case
(multiple-value-bind (arguments options) (adopt:parse-options *ui*)
(when (gethash 'help options)
(adopt:print-help-and-exit *ui*))
(when (gethash 'version options)
(format t "1.0.0~%")
(adopt:exit))
(destructuring-bind (pattern . files) arguments
(run pattern
files
:literal (gethash 'literal options))))
(error (c)
(adopt:print-error-and-exit c))))
(defun build ()
(sb-ext:save-lisp-and-die "search" :executable t :toplevel #'toplevel))
This is a typical way to use Adopt. There are three important functions here:
toplevel
function takes care of parsing arguments and exiting with an
appropriate status code if necessary.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.build
function dumps an executable binary. For more complicated
programs you might use something fancier, like ASDF or Shinmera's Deploy
library instead.In this example the toplevel
function first uses a handler-case
to trap all
errors. If any error occurs it will print the error message and exit, to avoid
confusing users by dropping them into a Lisp debugger REPL (which they probably
won't understand). If you're developing a program just for yourself, you might
want to omit this part and let yourself land in the debugger as usual.
Next it uses adopt:parse-options
to parse the command line arguments and
options. It them does some initial checks to see if the user wants --help
or
--version
information. If so, it prints the requested information and exits.
Otherwise it destructures the arguments into the expected items and calls run
with all the information it needs to do its job. If the destructuring-bind
fails an error will be signaled, and the handler-case
will print it and exit.
If you want to be a nice person you could check that the arguments
have the
correct shape first, and return a friendlier error message to your users if they
don't.
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:
--help
or --version
.-p/--paginate
and --no-pager
, where both options affect a single boolean flag.-v
option (more -v
's means more verbosity).--repository /path/to/repo
option, which specifies the path to a repository to work on.--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 library, for example, has a number of different "actions" to account to handle these various use cases. Adopt works differently: it uses an interface similar to reduce to let you do whatever you need.
First: before any options are parsed, all entries in the options hash table have
their values set to the :initial-value
given to make-option
(or nil
if
none was specified).
Next: When you create an option you must specify a :reduce
function that takes
the current value (and, for options that take a parameter, the given parameter)
and produces a new value each time the option is given.
You may also specify a :finally
function that will be called on the final
value after all parsing is done.
For convenience, if an option takes a parameter you may also specify a :key
function, which will be called on the given string before it is passed to the
:reduce
function. For example: you might use this for an option that takes
integers as arguments with something like :key #'parse-integer
.
The combination of these four pieces will let you do just about anything you might want. Let's look at how to do some common option parsing tasks using these as our building blocks.
To define an option that just tracks whether it's ever been given, you can do something like:
:::lisp
(defparameter *option-help*
(adopt:make-option 'help
:long "help"
:short #\h
:help "Display help and exit."
:initial-value nil
:reduce (lambda (current-value)
(declare (ignore current-value))
t)))
But since nil
is the default initial value and Common Lisp provides the handy
constantly
function, you can do this more concisely:
:::lisp
(defparameter *option-help*
(adopt:make-option 'help
:long "help"
:short #\h
:help "Display help and exit."
:reduce (constantly t)))
If you want to have multiple options that both affect the same key in the
results, you can use :result-key
to do this:
:::lisp
(defparameter *option-paginate*
(adopt:make-option 'paginate
:long "paginate"
:short #\p
:help "Turn pagination on."
:reduce (constantly t)))
(defparameter *option-no-paginate*
(adopt:make-option 'no-paginate
:result-key 'paginate
:long "no-paginate"
:short #\P
:help "Turn pagination off (the default)."
:reduce (constantly nil)))
The way we've written this, if the user gives multiple options the last-given one will take precedence. This is generally what you want, because it allows someone to add a shell alias with these options like this:
:::bash
alias g='git --paginate --color=always'
but still lets them override an option at runtime for a single invocation:
:::bash
g --no-paginate log
# expands to: git --paginate --color=always --no-paginate log
If the last-given option didn't take precedence, they'd have to fall back to the non-alias version of the command, and type out all the options they do want by hand. This is annoying, so it's usually better to let the last one win.
Making two separate options by hand can be tedious if you have a lot of boolean
options, so Adopt provides a make-boolean-options
function that will do some
of the boilerplate for you:
:::lisp
(adopt:make-boolean-options 'paginate
:long "paginate"
:short #\p
:help "Turn pagination on."
:help-no "Turn pagination off (the default).")
;; =>
#<ADOPT::OPTION PAGINATE p/paginate>
#<ADOPT::OPTION NO-PAGINATE P/no-paginate>
make-boolean-options
will try to guess at sensible values to reduce the
boilerplate you need to type:
:long
is "foo"
for the true option, the false option will be "no-foo"
unless overridden by :long-no
.:short
is #\f
for the true option, the false option will be
(char-upcase #\f)
unless overridden by :short-no
.foo
) will be used for the true option, and
a symbol with no-
prepended (e.g. no-foo
) will be used for the false
option unless overridden by :name-no
.:initial-value
will be nil
for the pair if not given.The two options are returned as separate values
. Adopt also provides
a defparameters
convenience macro to create special variables for them more
easily:
:::lisp
(defparameters (*option-paginate* *option-no-paginate*)
(adopt:make-boolean-options 'paginate
:long "paginate"
:short #\p
:help "Turn pagination on."
:help-no "Turn pagination off (the default)."))
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+))
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))
Collecting every parameter given can be done in a number of different ways. One strategy could be:
:::lisp
(defparameter *option-exclude*
(adopt:make-option 'exclude
:long "exclude"
:parameter "PATTERN"
:help "Exclude PATTERN (may be given multiple times)."
:initial-value nil
:reduce (lambda (patterns new)
(cons new patterns))))
You might notice that the :reduce
function here is just cons
with its
arguments flipped. Common Lisp doesn't have a function like Haskell's
flip,
so Adopt provides it:
:::lisp
(defparameter *option-exclude*
(adopt:make-option 'exclude
:long "exclude"
:parameter "PATTERN"
:help "Exclude PATTERN (may be given multiple times)."
:initial-value nil
:reduce (adopt:flip #'cons)))
Note that the result of this will be a fresh list of all the given parameters,
but their order will be reversed because cons
adds each new parameter to the
front of the list. If the order doesn't matter for what you're going to do with
it, you're all set. Otherwise, there are several ways to get around this
problem. The first is to add the parameter to the end of the list in the
:reduce
function:
:::lisp
(defparameter *option-exclude*
(adopt:make-option 'exclude
:long "exclude"
:parameter "PATTERN"
:help "Exclude PATTERN (may be given multiple times)."
:initial-value nil
:reduce (lambda (patterns new)
(append patterns (list new)))))
This is tedious and inefficient if you have a lot of arguments. If you don't
care much about argument parsing speed, Adopt provides a function called
collect
that does exactly this, so you don't have to type out that lambda
yourself (and nil
is the default initial value, so you don't need that
either):
:::lisp
(defparameter *option-exclude*
(adopt:make-option 'exclude
:long "exclude"
:parameter "PATTERN"
:help "Exclude PATTERN (may be given multiple times)."
:reduce #'adopt:collect))
A more efficient (though slightly uglier) solution would be to use nreverse
at
the end:
:::lisp
(defparameter *option-exclude*
(adopt:make-option 'exclude
:long "exclude"
:parameter "PATTERN"
:help "Exclude PATTERN (may be given multiple times)."
:reduce (adopt:flip #'cons)
:finally #'nreverse))
If you really need maximum efficiency when parsing command line options (you
probably don't) you could use a queue library, or use a vector and
vector-push-extend
, or anything else you might dream up. The combination of
:reduce
, :initial-value
, and :finally
will let you do just about anything.
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))))
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)
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}
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.
TODO: talk about Lisp runtime options vs program options.
You'll want to use :save-runtime-options t
in the call to sb-ext:save-lisp-and-die
.