├── README.md ├── cl-cli-test.lisp ├── cl-cli.asd └── cl-cli.lisp /README.md: -------------------------------------------------------------------------------- 1 | # CL-CLI 2 | 3 | Simple and easy to use modern command line argument parser for common lisp. 4 | 5 | 6 | ## Definition 7 | 8 | A typical command line consists of a *program name*, eventually *options*, 9 | *sub-commands*, *sub-command options* and a remaining. 10 | 11 | A modern command line would look like: 12 | 13 | ./tool --verbose --chroot /path/to/dir server start --restart \ 14 | --delay 10 instance1 instance2 15 | 16 | A program get its argument by the mean of list (or a vector) of strings such 17 | as: 18 | 19 | '("./tool" "--verbose" "--chroot" "/path/to/dir" "server" "start" 20 | "--restart" "--delay" "10" "instance1" "instance2") 21 | 22 | It is up to the programmer's responsibility to parse and interpret the 23 | command line. We can split that command line into several parts: 24 | 25 | 26 | * The *program name*: `./tool`. This is how the program is called. 27 | * Some *global options*: `--verbose --chroot /path/to/dir`. Each option can 28 | be a *switch*, such as `--verbose`, which is argument less, or can have 29 | one (or many) argument: `/path/to/dir` is an argument of the `--chroot` 30 | option. 31 | * A *sub-command*: `server start`. A sub command can be one or many verbs 32 | (or terms). 33 | * Some *sub-command options*: `--restart --delay 10`. This is the same as 34 | the *global options* but specific to the *sub-command*. 35 | * A *rest*: `instance1 instance2` its the command line remains after parsing 36 | process. 37 | 38 | ## CL-CLI usage 39 | 40 | 41 | ### Basic usage 42 | 43 | Think of a global option as a global variable as defined with 44 | `DEFPARAMETER` or `DEFVAR`. For example you would define `*DEBUG*`: 45 | 46 | (defparameter *debug* nil "Run in debug mode") 47 | 48 | Or in a more laconic way: 49 | 50 | (defvar *debug*) 51 | 52 | Now you only need to defined an option list suitable for `CL-CLI:PARSE-CLI`: 53 | 54 | CL-USER> (defparameter *options* 55 | '((*debug* nil "Run in debug mode" :alias ("-d")) 56 | (*dir* "/tmp" "Change to directory" :params ("DIR")))) 57 | 58 | 59 | Now you can parse the command line: 60 | 61 | CL-USER> (cl-cli:parse-cli '("./tool" "--dir" "/path/to/chroot" "--debug" 62 | "server" "start" "--restart" "--delay" "3" 63 | "instance1" "isntance2") *options*) 64 | (*DEBUG* *DIR*) 65 | (T "/path/to/chroot") 66 | NIL 67 | NIL 68 | NIL 69 | CL-USER> 70 | 71 | The first two returned values are the variable list and their values 72 | suitable for `PROGV`. You can now bind these variables and execute code 73 | using the `CL-CLI:WITH-ENVIRONMENT` macro: 74 | 75 | CL-USER> (multiple-value-bind (vars vals) 76 | (cl-cli:parse-cli '("./tool" "--dir" "/path/to/chroot" "--debug" 77 | "server" "start" "--restart" "--delay" "3" 78 | "instance1" "isntance2") *options*) 79 | (cl-cli:with-environment vars vals 80 | (format t "dir: ~a, debug: ~a~%" *dir* *debug*))) 81 | dir: /path/to/chroot, debug: T 82 | NIL 83 | 84 | The 3rd and 4th values are the sub-command to be run (if any, see below) and 85 | its arguments. The 5th value is the cli rest after parsing. 86 | 87 | ### Advanced usage 88 | 89 | A more advanced usage would use sub-commands. A sub-command defined by using 90 | `CL-CLI:DEFCOMMAND` which requires a dispatch verb list, an options list, a 91 | docstring and a body. 92 | 93 | CL-USER> (cl-cli:defcommand 94 | ("server" "start") 95 | ((restart nil "restart instead of start") 96 | (delay 2 "Second to wait" :params ("DELAY"))) 97 | "Start or restart server" 98 | (when *debug* 99 | (format t "Delay: ~a Restart: ~a~%" delay restart)) 100 | (format t "Server ~a in ~a~%" 101 | (if restart "restarted" "started") *dir*) 102 | 'start) 103 | #S(CL-CLI:SUB-COMMAND 104 | :VERBS ("server" "start") 105 | :OPTIONS ((RESTART NIL "restart instead of start") 106 | (DELAY 2 "Second to wait" :METAVARS ("DELAY"))) 107 | :DOCSTRING "Start or restart server" 108 | :FUNC #) 109 | 110 | 111 | 112 | `CL-CLI:DEFCOMMAND` create the function and returns a `CL-CLI:COMMAND` 113 | structure: 114 | 115 | CL-USER> (cl-cli:defcommand server-start 116 | (:help "Start server" 117 | :verbs ("server" "start") 118 | :options ((restart :help "restart instead of start") 119 | (:name delay :default 2 :params ("DELAY") :help "Seconds to wait"))) 120 | "Start or restart server" 121 | (when *debug* 122 | (format t "Delay: ~a Restart: ~a~%" delay restart)) 123 | (format t "Server ~a in ~a~%" (if restart "restarted" "started") *dir*)) 124 | #S(CL-CLI:SUB-COMMAND 125 | :NAME SERVER-START 126 | :FUNCTION # 127 | :ARGS # 128 | :VERBS ("server" "start") 129 | :DOCSTRING "Start or restart server" 130 | :HELP "Start server") 131 | CL-USER> 132 | 133 | 134 | A full working example would be something like: 135 | 136 | CL-USER> (let ((options 137 | (list 138 | (cl-cli:defoption '*debug* :default nil) 139 | (cl-cli:defoption '*dir* :default "/tmp" :params '("DIR")))) 140 | (argv '("./tool" "--dir" "/path/to/chroot" "--debug" 141 | "server" "start" "--restart" "--delay" "3" 142 | "instance1" "isntance2")) 143 | (sub-commands 144 | (list (cl-cli:defcommand server-start 145 | (:help "Start server" 146 | :verbs ("server" "start") 147 | :options ((restart 148 | :help "restart instead of start") 149 | (delay :default 2 :params '("DELAY") 150 | :help "Seconds to wait"))) 151 | "Start or restart server" 152 | (when *debug* 153 | (format t "Delay: ~a Restart: ~a~%" delay restart)) 154 | (format t "Server ~a in ~a~%" 155 | (if restart "restarted" "started") *dir*)) 156 | (cl-cli:defcommand server-stop 157 | (:help "Stop server" 158 | :verbs ("server" "stop") 159 | :options ((:name delay :default 2 :params '("DELAY") 160 | :help "Seconds to wait"))) 161 | "Stop server" 162 | (when *debug* 163 | (format t "Delay: ~a~%" delay)) 164 | (format t "Server stopped in ~a~%" *dir*))))) 165 | (cl-cli:run (:argv argv :options options :sub-commands sub-commands))) 166 | 167 | 168 | Delay: 3 Restart: T 169 | Server restarted in /path/to/chroot 170 | NIL 171 | CL-USER> 172 | 173 | ## API 174 | 175 | ### The options list 176 | 177 | This is a list of options you pass to te parser. Each option is defined as: 178 | 179 | (name default help &key long alias params type) 180 | 181 | * `name`: the option name, or variable to which the option is bound to. 182 | * `default`: The option default value. 183 | * `help`: a docstring that is used to display help. 184 | * `long`: the option long form generated against `name`. For example 185 | `*foo-bar*`: will result as `--foo-bar`. 186 | * `alias`: a list of option aliases. This is a good place to define 187 | shortcuts such as `-f` for `--foo` or `-b` for `--bar`. 188 | * `params`: a list of option parameters that define how many arguments the 189 | option requires. If `params` is `nil` no option is required. n the other 190 | hand if you need to defined a chroot diretory you can setup `params` to 191 | `("dir")`. Thus `--chroot` requires one mandatory argument. The `"dir"` 192 | string is used in help display. 193 | * `type`: A type to which the option should be converted to. 194 | 195 | ### The commands list 196 | 197 | As for options, the command list defined all known commands the parser 198 | should be aware of. A command is created using the `CL-CLI:DEFCOMMAND` 199 | macro. 200 | 201 | (defcommand verbs options positional docstring func) 202 | 203 | * `verbs`: A list of words that triggers the command. 204 | * `options`: a list of command options. 205 | * `positional`: a list of mandatory positional arguments (order 206 | maters). This is like an option but with no default value. 207 | * `docstring`: a string that is used to display help. 208 | * `func`: a function body that should handle the command. 209 | 210 | ### Functions 211 | 212 | #### parse-cli 213 | 214 | parse-cli (argv &optional options commands) 215 | 216 | Parse `ARGV` using `OPTIONS` both and `COMMANDS` directives. 217 | 218 | Returns 219 | * options variables 220 | * options values 221 | * matched command dispatch function 222 | * dispatch function keyword arguments 223 | * the rest of the command line argument" 224 | 225 | #### with-environment 226 | 227 | with-environment (vars vals &body body) 228 | 229 | Run `BODY` after binding `VARS` to `VALS`. 230 | 231 | The 2 first values of `parse-cli` can be used to feen `with-environment`. 232 | 233 | #### run-command 234 | 235 | run-command (argv &optional options commands) 236 | 237 | Run `parse-cli` and execute the matched dispatch function. 238 | 239 | #### help 240 | 241 | help (options sub-commands &key prog-name version prolog epilog) 242 | 243 | Display help screen. 244 | 245 | * `prog-name`: the program name. 246 | * `version`: the program version. 247 | * `prolog`: a text that should be displayed at the beging. 248 | * `epilog`: a text that should be diplayed at the end. 249 | 250 | 251 | 252 | 253 | ## Copyright 254 | 255 | Author: Sébastien Gross 256 | License: WTFPL, grab your copy here: http://www.wtfpl.net 257 | 258 | 259 | -------------------------------------------------------------------------------- /cl-cli-test.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:cl-cli) 2 | 3 | (defvar *debug*) 4 | (defvar *dir*) 5 | (defparameter *options* 6 | '((*debug* nil "Run in debug mode" :alias ("-d")) 7 | (*dir* "/tmp" "Change to directory" :metavars ("DIR")))) 8 | 9 | (defparameter *commands* 10 | (list 11 | (cl-cli::defcommand 12 | ("server" "start") 13 | ((restart nil "restart instead of start") 14 | (delay 2 "Second to wait" :metavars ("DELAY"))) 15 | "Start or restart server" 16 | (when *debug* 17 | (format t "Delay: ~a Restart: ~a~%" delay restart)) 18 | (format t "Server ~a in ~a~%" 19 | (if restart "restarted" "started") *dir*) 20 | "start") 21 | 22 | (cl-cli::defcommand 23 | ("server" "stop") 24 | ((delay 2 "Second to wait" :metavars ("DELAY"))) 25 | "Stop server" 26 | (when *debug* 27 | (format t "Delay: ~a~%" delay)) 28 | (format t "Server stopped in ~a~%" *dir*) 29 | "stop"))) 30 | 31 | ;; (cl-cli::run-command 32 | ;; '("./tool" "--dir" "/path/to/chroot" "--debug" 33 | ;; "server" "start" "--restart" "--delay" "3" 34 | ;; "instance1" "isntance2") *options* *commands*) 35 | -------------------------------------------------------------------------------- /cl-cli.asd: -------------------------------------------------------------------------------- 1 | 2 | (asdf:defsystem #:cl-cli 3 | :author ("Sébastien Gross") 4 | :version "0.2" 5 | :maintainer "Sébastien Gross" 6 | :licence "WTFPL" 7 | :description "Command line parser" 8 | :long-description "Simple and easy to use modern command line argument parser for common lisp." 9 | :depends-on (#:split-sequence) 10 | :pathname "." 11 | :components 12 | ((:file "cl-cli"))) 13 | -------------------------------------------------------------------------------- /cl-cli.lisp: -------------------------------------------------------------------------------- 1 | (defpackage #:cl-cli 2 | (:use #:cl) 3 | (:export 4 | #:defcommand 5 | #:defcommand-to-list 6 | #:parse-cli 7 | #:run-command 8 | #:with-environment 9 | #:help 10 | #:version)) 11 | 12 | 13 | (in-package #:cl-cli) 14 | 15 | 16 | 17 | (define-condition option-requires-argument (error) 18 | ((option :initarg :option :reader option-requires-argument-option) 19 | (nargs :initarg :nargs :reader option-requires-argument-nargs)) 20 | (:report 21 | (lambda (condition stream) 22 | (format stream "Option ~S requires ~a argument~:p" 23 | (option-requires-argument-option condition) 24 | (option-requires-argument-nargs condition))))) 25 | 26 | (define-condition bad-argument-type (error) 27 | ((option :initarg :option :reader bad-argument-type-option) 28 | (type :initarg :type :reader bad-argument-type-type)) 29 | (:report 30 | (lambda (condition stream) 31 | (format stream "Option ~S requires a ~(~a~) argument" 32 | (bad-argument-type-option condition) 33 | (bad-argument-type-type condition))))) 34 | 35 | (define-condition not-enougth-pos-args (error) 36 | ((command :initarg :command :reader not-enougth-pos-args-command) 37 | (count :initarg :count :reader not-enougth-pos-args-count) 38 | (items :initarg :items :reader not-enougth-pos-args-items)) 39 | (:report 40 | (lambda (condition stream) 41 | (format stream "Command \"~{~a~^ ~}\" requires ~a positional argument~:p: ~{~a~^, ~}." 42 | (not-enougth-pos-args-command condition) 43 | (not-enougth-pos-args-count condition) 44 | (not-enougth-pos-args-items condition))))) 45 | 46 | 47 | 48 | (defun %symbol-to-option-string (symbol) 49 | "Convert SYMBOL to its argument string representation by prepending with 50 | double dash \"--\" and removing fancy chars such as \"*\"." 51 | (concatenate 'string "--" 52 | ;; (remove #\* string) seems to be less speed-efficient. 53 | (loop for c across (string-downcase (symbol-name symbol)) 54 | unless (char= c #\*) 55 | collecting c))) 56 | 57 | 58 | (defun %make-options-list (options) 59 | "Convert given OPTIONS list into a list of CL-CLI:OPTION" 60 | (loop for option in options 61 | for long = (%symbol-to-option-string (car option)) 62 | collect (apply #'make-option (append option `(:long ,long))))) 63 | 64 | 65 | 66 | 67 | (defun consume-option (args option) 68 | "Extract all OPTION arguments from ARGS. 69 | Return both consumed arguments count and the arguments" 70 | (let ((nargs (or (length (opt-params option)) 0))) 71 | ;;(format t " ~a ~a~%" nargs option) 72 | (when (< (length args) nargs) 73 | (error 'option-requires-argument 74 | :option (opt-long option) :nargs nargs)) 75 | (let ((argument 76 | (cond 77 | ((= 0 nargs) t) 78 | ((= 1 nargs) (car args)) 79 | (t (subseq args 0 (1- nargs))))) 80 | (type (opt-type option))) 81 | (values nargs 82 | (if type 83 | (handler-case 84 | (if (atom argument) 85 | (coerce (read-from-string argument) type) 86 | (loop for a in argument 87 | collect (coerce (read-from-string a) type))) 88 | (simple-type-error () 89 | (error 'bad-argument-type :option (opt-long option) 90 | :type type))) 91 | argument))))) 92 | 93 | 94 | (defstruct 95 | (option 96 | (:conc-name opt-) 97 | (:constructor make-option (name default help &key long alias params type ))) 98 | name 99 | default 100 | help 101 | long 102 | alias 103 | params 104 | type) 105 | 106 | 107 | 108 | (defun parse-options (argv options) 109 | (let ((opts (make-hash-table :test 'equal)) 110 | (values (make-hash-table :test 'equal)) 111 | (idx 0) vars vals) 112 | 113 | ;; Setup lookup has 114 | (loop for option in (%make-options-list options) 115 | do (progn 116 | (setf (gethash (opt-name option) values) (opt-default option)) 117 | ;; For long option 118 | (setf (gethash (opt-long option) opts) option) 119 | ;; For alias as well 120 | (loop for alias in (opt-alias option) 121 | do (setf (gethash alias opts) option)))) 122 | 123 | (loop for i from idx below (length argv) 124 | for cur-opt = (nth i argv) 125 | for cur-opt-def = (gethash cur-opt opts) 126 | ;;do (format t "Option[~a]: ~a~%" i cur-opt-def) 127 | while cur-opt-def 128 | do (progn 129 | (multiple-value-bind (nargs vals) 130 | (consume-option (subseq argv (1+ i)) cur-opt-def) 131 | (setf i (+ i nargs)) 132 | (setf (gethash (opt-name cur-opt-def) values) vals))) 133 | finally (setf idx i)) 134 | 135 | (loop for k being the hash-keys of values 136 | do (progn 137 | (push k vars) 138 | (push (gethash k values) vals))) 139 | 140 | (values (subseq argv idx) opts vars vals))) 141 | 142 | 143 | (defstruct 144 | (sub-command 145 | (:conc-name sub-) 146 | (:constructor make-sub-command (verbs options positional docstring func))) 147 | verbs 148 | options 149 | positional 150 | docstring 151 | func) 152 | 153 | 154 | (defun parse-command-args (args) 155 | "Parse ARGS and return 3 lists: 156 | 157 | - A function suitable lambda-list 158 | - A list of all positional arguments 159 | - A list of all key arguments. 160 | 161 | ARGS is a lambda-list as defined for defcommand." 162 | (let (positional keys lambda-list) 163 | (loop for arg in args 164 | with in 165 | do (cond 166 | ((eq '&key arg) (progn 167 | (push arg lambda-list) 168 | (setf in arg))) 169 | ((eq '&key in) 170 | (progn 171 | (push (list (car arg) (nth 1 arg)) lambda-list) 172 | (push arg keys))) 173 | (t 174 | (progn 175 | (push (car arg) lambda-list) 176 | (push (append (list (car arg) nil) (cdr arg)) positional))))) 177 | (values 178 | (reverse lambda-list) 179 | (reverse positional) 180 | (reverse keys)))) 181 | 182 | (defmacro defcommand (verbs options docstring &body body) 183 | "Create a new command defined by VERBS list (dispatch arguments) a list of 184 | OPTIONS, a DOCSTRING and use BODY as dispatch function." 185 | (multiple-value-bind (lambda-list positional keys) 186 | (parse-command-args options) 187 | `(make-sub-command ',verbs ',keys ',positional ,docstring 188 | (lambda (,@lambda-list) ,@body)))) 189 | 190 | (defmacro defcommand-to-list (var verbs options docstring &body body) 191 | "Call DEFCOMMAND and append result to VAR." 192 | `(setq ,var 193 | (nconc ,var 194 | (list (defcommand ,verbs ,options ,docstring ,@body))))) 195 | 196 | 197 | (defun convert-vars-vals-to-keys (vars vals) 198 | "Convert VARS and VALS lists to a keyword plist. 199 | 200 | Example: 201 | '(VAR1 VAR1) (val1 val2) => (:VAR1 val1 :VAR2 val2)" 202 | 203 | (loop for i below (length vars) 204 | nconc (list (intern (format nil "~a" (nth i vars)) "KEYWORD") 205 | (nth i vals)))) 206 | 207 | (defun get-positional-args(argv cmd) 208 | (let ((nargs (length (sub-positional cmd)))) 209 | (when (> nargs 0) 210 | (when (< (length argv) nargs) 211 | (error 'not-enougth-pos-args 212 | :command (sub-verbs cmd) 213 | :count nargs 214 | :items (mapcar #'car (sub-positional cmd)))) 215 | (values 216 | (subseq argv nargs) 217 | (subseq argv 0 nargs))))) 218 | 219 | (defun parse-commands(argv commands) 220 | (let* ((len-argv (length argv)) 221 | (cmd (loop for command in commands 222 | for verbs = (sub-verbs command) 223 | when (and (>= len-argv (length verbs)) 224 | (equal verbs (subseq argv 0 (length verbs)))) 225 | return command))) 226 | (if (not cmd) 227 | argv 228 | (multiple-value-bind (argv opts-hash opts-vars opts-values) 229 | (parse-options 230 | (subseq argv (length (sub-verbs cmd))) 231 | (sub-options cmd)) 232 | 233 | (multiple-value-bind (argv positional) 234 | (get-positional-args argv cmd) 235 | 236 | (values argv opts-hash 237 | (append positional 238 | (convert-vars-vals-to-keys opts-vars opts-values)) 239 | cmd)))))) 240 | 241 | (defun parse-cli (argv &optional options commands) 242 | "Parse ARGV using OPTIONS both and COMMANDS directives. 243 | 244 | Return: 245 | - options variables 246 | - options values 247 | - matched command dispatch function 248 | - dispatch function keyword arguments 249 | - the rest of the command line argument" 250 | (let ((argv (cdr argv))) 251 | (multiple-value-bind (argv opts-hash opts-vars opts-values) 252 | (parse-options argv options) 253 | (declare (ignore opts-hash)) 254 | ;;(format t "ARGV: ~S~%" argv) 255 | (multiple-value-bind (argv sub-opt-hash sub-opts-keys sub) 256 | (parse-commands argv commands) 257 | (declare (ignore sub-opt-hash)) 258 | ;;(format t "ARGV: ~S~%" argv) 259 | (values opts-vars opts-values 260 | (when sub (sub-func sub)) sub-opts-keys argv))))) 261 | 262 | 263 | (defmacro with-environment (vars vals &body body) 264 | (let ((%vars (gensym)) 265 | (%vals (gensym))) 266 | `((lambda (,%vars ,%vals) 267 | (progv ,%vars ,%vals 268 | ,@body)) ,vars ,vals))) 269 | 270 | (defun run-command (argv &optional options commands) 271 | "Parse ARGV using OPTIONS both and COMMANDS directives." 272 | (multiple-value-bind (opts-vars opts-values sub-func sub-opts argv) 273 | (parse-cli argv options commands) 274 | (values 275 | (when sub-func 276 | (with-environment (append opts-vars '(*argv*)) 277 | (append opts-values (list argv)) 278 | (apply sub-func sub-opts))) 279 | opts-vars opts-values sub-func sub-opts argv))) 280 | 281 | 282 | (defun %print-option(option) 283 | (format t "~2T~@[~{~a,~}~]~a~20T~@[ <~{~(~a~)~^ ~}>~]~ 284 | ~35T~{~<~%~35T~0,79:;~a~>~^ ~}~%" 285 | (opt-alias option) 286 | (opt-long option) 287 | (opt-params option) 288 | (split-sequence:split-sequence 289 | #\ (format nil "~a~:[~; (default: ~:*~@a)~]" 290 | (or (opt-help option) "") 291 | (opt-default option))))) 292 | 293 | (defun %print-positional(option) 294 | (format t "~2T<~(~a~)>~35T~{~<~%~35T~0,79:;~a~>~^ ~}~%" 295 | (opt-name option) 296 | (split-sequence:split-sequence 297 | #\ (or (opt-help option) "")))) 298 | 299 | 300 | (defun version (prog-name version &key copyright quit) 301 | (format t "~@[~a~]~@[ version ~a~]~%" prog-name version) 302 | (format t "~@[Built using ~a~]~%" (uiop/os:implementation-identifier)) 303 | (format t "~@[~%~a~%~]" copyright) 304 | 305 | (when quit 306 | (uiop:quit quit))) 307 | 308 | 309 | (defun help (options sub-commands &key prog-name version prolog epilog) 310 | (let ((options (%make-options-list options))) 311 | 312 | 313 | (format t "~@[~a~]~@[ version ~a~]~%~%" prog-name version) 314 | 315 | (when prolog 316 | (format t "~{~<~%~0,79:;~a~>~^ ~}~%~%" 317 | (split-sequence:split-sequence 318 | #\ prolog))) 319 | 320 | (if sub-commands 321 | (loop for sub in sub-commands 322 | do (format t 323 | "~@[~a~]~:[~; [ OPTIONS ]~] ~{~a~^ ~}~ 324 | ~:[~; [ OPTIONS ]~] ~{<~(~a~)>~^ ~}~%" 325 | prog-name 326 | options 327 | (sub-verbs sub) 328 | (sub-options sub) 329 | (mapcar #'car (sub-positional sub)))) 330 | (format t "~@[~a~]~:[~; [ OPTIONS ]~]~%" 331 | prog-name options)) 332 | 333 | (when options 334 | (format t "~%Global options:~%") 335 | (loop for option in options 336 | do (%print-option option))) 337 | 338 | (when sub-commands 339 | (format t "~%Sub commands:~%") 340 | (loop for sub-command in sub-commands 341 | do (progn 342 | (format t "~%~1T~{~a~^ ~}:~20T~{~<~%~20T~0,79:;~a~>~^ ~}~%" 343 | (sub-verbs sub-command) 344 | (split-sequence:split-sequence 345 | #\ (sub-docstring sub-command))) 346 | (loop for option in (%make-options-list (sub-options sub-command)) 347 | do (%print-option option)) 348 | (loop for option in (%make-options-list (sub-positional sub-command)) 349 | do (%print-positional option))))) 350 | 351 | (when epilog 352 | (format t "~%~{~<~%~0,79:;~a~>~^ ~}~%" 353 | (split-sequence:split-sequence 354 | #\ epilog))))) 355 | --------------------------------------------------------------------------------