# HG changeset patch # User Steve Losh # Date 1607816435 18000 # Node ID b9b3d5f9211c5e1d41315f4b13648a0eba32f8e4 # Parent d81c98692e73b08d84dd86d0b6878c7043dab116 Add make-boolean-options and defparameters, fix :help style in docs diff -r d81c98692e73 -r b9b3d5f9211c adopt.asd --- a/adopt.asd Thu Jan 30 19:27:27 2020 -0500 +++ b/adopt.asd Sat Dec 12 18:40:35 2020 -0500 @@ -4,7 +4,7 @@ :homepage "https://docs.stevelosh.com/adopt/" :license "MIT" - :version "1.0.1" + :version "1.1.0" :depends-on (:bobbin :split-sequence) diff -r d81c98692e73 -r b9b3d5f9211c docs/01-usage.markdown --- a/docs/01-usage.markdown Thu Jan 30 19:27:27 2020 -0500 +++ b/docs/01-usage.markdown Sat Dec 12 18:40:35 2020 -0500 @@ -37,7 +37,8 @@ `make-interface` takes several required arguments: * `:name` is the name of the program. -* `:summary` is a concise one-line summary of what it does. +* `: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. @@ -240,21 +241,21 @@ (defparameter *option-version* (adopt:make-option 'version :long "version" - :help "display version information and exit" + :help "Display version and exit." :reduce (constantly t))) (defparameter *option-help* (adopt:make-option 'help :long "help" :short #\h - :help "display help information and exit" + :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" + :help "Treat PATTERN as a literal string instead of a regular expression." :reduce (constantly t))) (defparameter *ui* @@ -280,16 +281,21 @@ ; 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 + ; --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: @@ -341,7 +347,7 @@ :result-key 'pattern-is-literal :long "literal" :short #\l - :help "treat PATTERN as a literal string instead of a regular expression" + :help "Treat PATTERN as a literal string instead of a regular expression." :reduce (constantly t))) ;; … @@ -484,7 +490,7 @@ (adopt:make-option 'help :long "help" :short #\h - :help "display help information and exit" + :help "Display help and exit." :initial-value nil :reduce (lambda (current-value) (declare (ignore current-value)) @@ -499,7 +505,7 @@ (adopt:make-option 'help :long "help" :short #\h - :help "display help information and exit" + :help "Display help and exit." :reduce (constantly t))) ### Boolean Options @@ -512,7 +518,7 @@ (adopt:make-option 'paginate :long "paginate" :short #\p - :help "turn pagination on" + :help "Turn pagination on." :reduce (constantly t))) (defparameter *option-no-paginate* @@ -520,7 +526,7 @@ :result-key 'paginate :long "no-paginate" :short #\P - :help "turn pagination off (the default)" + :help "Turn pagination off (the default)." :reduce (constantly nil))) The way we've written this, if the user gives multiple options the last-given @@ -540,6 +546,44 @@ 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).") + ;; => + # + # + +`make-boolean-options` will try to guess at sensible values to reduce the +boilerplate you need to type: + +* If `:long` is `"foo"` for the true option, the false option will be `"no-foo"` + unless overridden by `:long-no`. +* If `:short` is `#\f` for the true option, the false option will be + `(char-upcase #\f)` unless overridden by `:short-no`. +* The given option name (e.g. `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).")) + ### Counting Options To define an option that counts how many times it's been given, like SSH's `-v`, @@ -549,7 +593,7 @@ (defparameter *option-verbosity* (adopt:make-option 'verbosity :short #\v - :help "output more verbose logs" + :help "Output more verbose logs." :initial-value 0 :reduce #'1+)) @@ -564,7 +608,7 @@ :parameter "PATTERN" :long "repository" :short #\R - :help "path to the repository (default .)" + :help "Path to the repository (default .)." :initial-value "." :reduce (lambda (prev new) (declare (ignore prev)) @@ -583,7 +627,7 @@ (adopt:make-option 'repository :long "repository" :short #\R - :help "path to the repository (default .)" + :help "Path to the repository (default .)." :initial-value "." :reduce #'adopt:last)) @@ -597,7 +641,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude PATTERN (may be given multiple times)" + :help "Exclude PATTERN (may be given multiple times)." :initial-value nil :reduce (lambda (patterns new) (cons new patterns)))) @@ -612,7 +656,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude PATTERN (may be given multiple times)" + :help "Exclude PATTERN (may be given multiple times)." :initial-value nil :reduce (adopt:flip #'cons))) @@ -628,7 +672,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude PATTERN (may be given multiple times)" + :help "Exclude PATTERN (may be given multiple times)." :initial-value nil :reduce (lambda (patterns new) (append patterns (list new))))) @@ -644,7 +688,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude PATTERN (may be given multiple times)" + :help "Exclude PATTERN (may be given multiple times)." :reduce #'adopt:collect)) A more efficient (though slightly uglier) solution would be to use `nreverse` at @@ -655,7 +699,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude PATTERN (may be given multiple times)" + :help "Exclude PATTERN (may be given multiple times)." :reduce (adopt:flip #'cons) :finally #'nreverse)) @@ -698,14 +742,14 @@ :::lisp (defparameter *option-help* (adopt:make-option 'help - :help "display help and exit" + :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" + :help "Treat PATTERN as a literal string instead of a regex." :long "literal" :short #\l :reduce (constantly t))) @@ -713,14 +757,14 @@ (defparameter *option-no-literal* (adopt:make-option 'no-literal :result-key 'literal - :help "treat PATTERN as a regex (the default)" + :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)" + :help "Match case-sensitively (the default)." :long "case-sensitive" :short #\c :initial-value t @@ -728,7 +772,7 @@ (defparameter *option-case-insensitive* (adopt:make-option 'case-insensitive - :help "ignore case when matching" + :help "Ignore case when matching." :long "case-insensitive" :short #\C :result-key 'case-sensitive @@ -736,13 +780,13 @@ (defparameter *option-color* (adopt:make-option 'color - :help "highlight matches with 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)" + :help "Don't highlight matches (the default)." :long "no-color" :result-key 'color :reduce (constantly nil))) @@ -750,7 +794,7 @@ (defparameter *option-context* (adopt:make-option 'context :parameter "N" - :help "show N lines of context (default 0)" + :help "Show N lines of context (default 0)." :long "context" :short #\U :initial-value 0 @@ -852,7 +896,7 @@ :parameter "N" :long "times" :initial-value 1 - :help "say meow N times (default 1)" + :help "Say meow N times (default 1)." :reduce #'adopt:last :key #'parse-integer)))) @@ -923,7 +967,7 @@ (adopt:make-option 'exclude :long "exclude" :parameter "PATTERN" - :help "exclude 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))) diff -r d81c98692e73 -r b9b3d5f9211c docs/02-reference.markdown --- a/docs/02-reference.markdown Thu Jan 30 19:27:27 2020 -0500 +++ b/docs/02-reference.markdown Sat Dec 12 18:40:35 2020 -0500 @@ -44,6 +44,37 @@ Convenience macro for `(defparameter ,var (format nil ,string ,@args))`. +### `DEFPARAMETERS` (macro) + + (DEFPARAMETERS PARAMETERS VALUES-FORM) + +Convenience macro for `defparameter`ing multiple variables at once. + + `parameters` must be a list of special variable names suitable for giving to + `defparameter`. + + `values-form` must be an expression that returns as many values as parameters + in the parameter list. Each parameter will be set to the corresponding value. + + This can be handy when using `make-boolean-options` to create two `option`s at + once and assign them to special variables. + + Examples: + + (defparameters (*a* *b*) (truncate 100 3)) + (list *a* *b*) + ; => + ; (33 1) + + (defparameters (*option-foo* *option-no-foo*) + (make-boolean-options 'foo + :help "Foo the widgets during the run." + :help-no "Do not foo the widgets during the run (the default)." + :long "foo" + :short #f)) + + + ### `DISCARD-OPTION` (function) (DISCARD-OPTION CONDITION) @@ -110,6 +141,53 @@ +### `MAKE-BOOLEAN-OPTIONS` (function) + + (MAKE-BOOLEAN-OPTIONS NAME &KEY + (NAME-NO (INTERN (CONCATENATE 'STRING (STRING 'NO-) (STRING NAME)))) LONG + (LONG-NO (WHEN LONG (FORMAT NIL no-~A LONG))) SHORT + (SHORT-NO (WHEN SHORT (CHAR-UPCASE SHORT))) (RESULT-KEY NAME) HELP + HELP-NO MANUAL MANUAL-NO INITIAL-VALUE) + +Create and return a pair of boolean options, suitable for use in an interface. + + This function reduces some of the boilerplate when creating two `option`s for + boolean values, e.g. `--foo` and `--no-foo`. It will try to guess at an + appropriate name, long option, short option, and result key, but you can + override them with the `…-no` keyword options as needed. + + The two options will be returned as two separate values — you can use + `defparameters` to conveniently bind them to two separate variables if + desired. + + Example: + + (defparameters (*option-debug* *option-no-debug*) + (make-boolean-options 'debug + :long "debug" + :short #d + :help "Enable the Lisp debugger." + :help-no "Disable the Lisp debugger (the default).")) + + ;; is roughly equivalent to: + + (defparameter *option-debug* + (make-option 'debug + :long "debug" + :short #d + :help "Enable the Lisp debugger." + :initial-value nil + :reduce (constantly t)) + + (defparameter *option-no-debug* + (make-option 'no-debug + :long "no-debug" + :short #D + :help "Disable the Lisp debugger (the default)." + :reduce (constantly nil)) + + + ### `MAKE-GROUP` (function) (MAKE-GROUP NAME &KEY TITLE HELP MANUAL OPTIONS) diff -r d81c98692e73 -r b9b3d5f9211c docs/03-changelog.markdown --- a/docs/03-changelog.markdown Thu Jan 30 19:27:27 2020 -0500 +++ b/docs/03-changelog.markdown Sat Dec 12 18:40:35 2020 -0500 @@ -5,6 +5,11 @@ [TOC] +1.1.0 +----- + +Added `make-boolean-options` and `defparameters` for convenience. + 1.0.1 ----- diff -r d81c98692e73 -r b9b3d5f9211c src/main.lisp --- a/src/main.lisp Thu Jan 30 19:27:27 2020 -0500 +++ b/src/main.lisp Sat Dec 12 18:40:35 2020 -0500 @@ -202,6 +202,107 @@ (typep object 'option)) +(defmacro defparameters (parameters values-form) + "Convenience macro for `defparameter`ing multiple variables at once. + + `parameters` must be a list of special variable names suitable for giving to + `defparameter`. + + `values-form` must be an expression that returns as many values as parameters + in the parameter list. Each parameter will be set to the corresponding value. + + This can be handy when using `make-boolean-options` to create two `option`s at + once and assign them to special variables. + + Examples: + + (defparameters (*a* *b*) (truncate 100 3)) + (list *a* *b*) + ; => + ; (33 1) + + (defparameters (*option-foo* *option-no-foo*) + (make-boolean-options 'foo + :help \"Foo the widgets during the run.\" + :help-no \"Do not foo the widgets during the run (the default).\" + :long \"foo\" + :short #\f)) + + " + `(progn + ,@(loop :for parameter :in parameters + :collect `(defparameter ,parameter nil)) + (setf (values ,@parameters) ,values-form) + ',parameters)) + +(defun make-boolean-options + (name &key + (name-no (intern (concatenate 'string (string 'no-) (string name)))) + long + (long-no (when long (format nil "no-~A" long))) + short + (short-no (when short (char-upcase short))) + (result-key name) + help + help-no + manual + manual-no + initial-value) + "Create and return a pair of boolean options, suitable for use in an interface. + + This function reduces some of the boilerplate when creating two `option`s for + boolean values, e.g. `--foo` and `--no-foo`. It will try to guess at an + appropriate name, long option, short option, and result key, but you can + override them with the `…-no` keyword options as needed. + + The two options will be returned as two separate values — you can use + `defparameters` to conveniently bind them to two separate variables if + desired. + + Example: + + (defparameters (*option-debug* *option-no-debug*) + (make-boolean-options 'debug + :long \"debug\" + :short #\d + :help \"Enable the Lisp debugger.\" + :help-no \"Disable the Lisp debugger (the default).\")) + + ;; is roughly equivalent to: + + (defparameter *option-debug* + (make-option 'debug + :long \"debug\" + :short #\d + :help \"Enable the Lisp debugger.\" + :initial-value nil + :reduce (constantly t)) + + (defparameter *option-no-debug* + (make-option 'no-debug + :long \"no-debug\" + :short #\D + :help \"Disable the Lisp debugger (the default).\" + :reduce (constantly nil)) + + " + (values (adopt:make-option name + :result-key result-key + :long long + :short short + :help help + :manual manual + :initial-value initial-value + :reduce (constantly t)) + (adopt:make-option name-no + :result-key result-key + :long long-no + :short short-no + :help help-no + :manual manual-no + :reduce (constantly nil)))) + + (defclass* group name title help manual options) diff -r d81c98692e73 -r b9b3d5f9211c src/package.lisp --- a/src/package.lisp Thu Jan 30 19:27:27 2020 -0500 +++ b/src/package.lisp Sat Dec 12 18:40:35 2020 -0500 @@ -2,8 +2,10 @@ (:use :cl) (:export :define-string + :defparameters :make-option + :make-boolean-options :make-group :make-interface diff -r d81c98692e73 -r b9b3d5f9211c test/tests.lisp --- a/test/tests.lisp Thu Jan 30 19:27:27 2020 -0500 +++ b/test/tests.lisp Sat Dec 12 18:40:35 2020 -0500 @@ -189,6 +189,21 @@ :reduce #'adopt:collect :key #'length)))) +(adopt:defparameters (*bool* *no-bool*) + (adopt:make-boolean-options 'bool + :long "bool" + :short #\b + :help "Bool yes." + :help-no "Bool no.")) + +(defparameter *bools* + (adopt:make-interface + :name "bools" + :summary "testing boolean options" + :help "this interface tests booleans" + :usage "[OPTIONS]" + :contents (list *bool* *no-bool*))) + (define-test noop (check *noop* "" @@ -324,3 +339,10 @@ :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"))))) + +(define-test boolean-options + (check *bools* "" '() (result 'bool nil)) + (check *bools* "--bool" '() (result 'bool t)) + (check *bools* "--bool --no-bool" '() (result 'bool nil)) + (check *bools* "-b" '() (result 'bool t)) + (check *bools* "-b -B" '() (result 'bool nil)))