# HG changeset patch # User Steve Losh # Date 1562011403 14400 # Node ID d70dd2b9e3c2d186d96224d514016e4ddbede054 # Parent bc8760190ed767012e3d031632f539a7c8710a8e# Parent 8115d14339b38bacac966caadff364b954ecc92e Merge. 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:** * **Mercurial:** * **Git:** +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") - # - -`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") + ; # 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 (#) + +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 . 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")))))