b9b3d5f9211c v1.1.0

Add make-boolean-options and defparameters, fix :help style in docs
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sat, 12 Dec 2020 18:40:35 -0500
parents d81c98692e73
children d914d5aad7dc
branches/tags v1.1.0
files adopt.asd docs/01-usage.markdown docs/02-reference.markdown docs/03-changelog.markdown src/main.lisp src/package.lisp test/tests.lisp

Changes

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).")
+    ;; =>
+    #<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:
+
+* 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)))