Mercurial > core / lisp/lib/cli/clap/cmd.lisp
changeset 643: |
f901de70a80e |
parent: |
cc13027df6fa
|
child: |
3e6a17fb5712 |
author: |
Richard Westhaver <ellis@rwest.io> |
date: |
Tue, 10 Sep 2024 21:26:30 -0400 |
permissions: |
-rw-r--r-- |
description: |
opt fixes and test updates |
1 ;;; cli/clap/cmd.lisp --- Clap Commands 3 ;; Command Objects used to build CLI Applications. 10 (in-package :cli/clap/obj) 13 ;; name slot is required and must be a string 14 ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string) 15 (opts :initarg :opts :initform (make-array 0 :element-type 'cli-opt :adjustable t) 16 :accessor cli-opts :type (vector cli-opt)) 17 (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd :adjustable t) 18 :accessor cli-cmds :type (vector cli-cmd)) 19 (thunk :initform #'default-thunk :initarg :thunk :accessor cli-thunk :type function-lambda-expression) 20 (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean) 21 (description :initarg :description :accessor cli-description :type string) 22 (args :initform nil :initarg :args :accessor cli-cmd-args)) 23 (:documentation "CLI command class inherited by both the 'main' command which is executed when 24 a CLI is called without arguments, and all subcommands.")) 26 (defmethod initialize-instance :after ((self cli-cmd) &key) 27 (with-slots (name thunk opts cmds) self 28 (unless (stringp name) (setf name (format nil "~(~A~)" name))) 29 (unless (vectorp cmds) (setf cmds (make-cmds cmds))) 30 (unless (vectorp opts) (setf opts (make-opts opts))) 31 (when (symbolp thunk) (setf thunk (symbol-function thunk))) 34 (defmethod print-object ((self cli-cmd) stream) 35 (print-unreadable-object (self stream :type t) 36 (format stream "~A :opts ~A :cmds ~A :args ~A" 38 (length (cli-opts self)) 39 (length (cli-cmds self)) 40 (length (cli-cmd-args self))))) 42 (defmethod print-usage ((self cli-cmd) &optional stream) 43 (with-slots (opts cmds) self 44 (format stream "~(~A~) ~A~A~A" 46 (if-let ((d (and (slot-boundp self 'description) (cli-description self)))) 51 (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil)))) 54 (format nil "~{!~A~}" (loop for c across cmds collect (print-usage c nil))))))) 56 (defmethod push-cmd ((self cli-cmd) (place cli-cmd)) 57 (vector-push self (cli-cmds place))) 59 (defmethod push-opt ((self cli-opt) (place cli-cmd)) 60 (vector-push self (cli-opts place))) 62 (defmethod pop-cmd ((self cli-cmd)) 63 (vector-pop (cli-cmds self))) 65 (defmethod pop-opt ((self cli-opt)) 66 (vector-pop (cli-opts self))) 68 (defmethod cli-equal ((a cli-cmd) (b cli-cmd)) 69 (with-slots (name opts cmds) a 70 (with-slots ((bn name) (bo opts) (bc cmds)) b 71 (and (string= name bn) 72 (if (and (null opts) (null bo)) 74 (unless (member nil (loop for oa across opts 76 collect (cli-equal oa ob))) 78 (if (and (null cmds) (null bc)) 80 (unless (member nil (loop for ca across cmds 82 collect (cli-equal ca cb))) 85 (defmethod find-cmd ((self cli-cmd) name &optional active) 86 (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=))) 88 ;; maybe issue warning here? report to user 94 (defmethod active-cmds ((self cli-cmd)) 95 (remove-if-not #'cli-lock-p (cli-cmds self))) 97 (defmethod find-opts ((self cli-cmd) name &key active recurse) 100 (when-let ((found (find o (cli-opts obj) :key #'cli-opt-name :test 'equal))) 102 (when (and recurse (cli-cmds self)) 103 (loop for c across (cli-cmds self) 107 (setf ret (remove-if-not #'cli-lock-p ret))) 110 (defmethod active-opts ((self cli-cmd) &optional global) 113 #'active-global-opt-p 117 (defmethod find-short-opts ((self cli-cmd) ch &key recurse) 119 (flet ((%find (ch obj) 120 (when-let ((found (find ch (cli-opts obj) :key #'cli-opt-name :test #'opt-string-prefix-eq))) 122 (when (and recurse (cli-cmds self)) 123 (loop for c across (cli-cmds self) 128 (declaim (inline solop)) 130 (and (= 0 (length (active-cmds self)) (length (active-opts self))))) 132 (defmethod proc-args ((self cli-cmd) args) 133 "Process ARGS into an ast. Each element of the ast is a node with a 134 :kind slot, indicating the type of node and a :form slot which stores 137 For now we parse group separators '--' and insert a nil into the tree, 138 this will likely change to generating a new branch in the ast as it 141 (let ((holes)) ;; list of arg indexes which can be skipped since they're 142 ;; consumed by an opt 144 for i below (length args) 145 for (a . args) on args 147 do (continue) ;; skip args which have been consumed already 150 collect (make-cli-node 'arg a) ; always treat single-char as arg 152 if (short-opt-p a) ;; SHORT OPT 154 (if-let ((o (find-short-opts self (aref a 1) :recurse t))) 155 (%compose-short-opt (car o) a) 156 (make-cli-node 'arg a)) 158 if (long-opt-p a) ;; LONG OPT 160 (let ((o (find-opts self (string-left-trim "-" a) :recurse t)) 161 (has-eq (long-opt-has-eq-p a))) 164 (setf (cli-opt-val o) (cdr has-eq)) 165 (make-cli-node 'opt o)) 166 ((and (not has-eq) o) 167 (prog1 (%compose-long-opt (car o) args) 168 (push (1+ i) holes))) 169 ((and has-eq (not o)) 170 (warn 'warning "opt not recognized" a) 171 (let ((val (cdr has-eq))) 172 (make-cli-node 'opt (make-cli-opt :name (car has-eq) :kind (type-of val) :val val)))) 173 (t ;; (not o) (not has-eq) 174 (warn 'warning "opt not recognized" a) 175 (make-cli-node 'arg a)))) 183 (let ((cmd (find-cmd self a))) 186 (make-cli-node 'cmd (find-cmd self a)) 188 (make-cli-node 'arg a))))))) 190 (defmethod install-ast ((self cli-cmd) (ast cli-ast)) 191 "Install the given AST, recursively filling in value slots." 192 (with-slots (cmds opts) self 193 ;; we assume all nodes in the ast have been validated and the ast 194 ;; itself is consumed. validation is performed in proc-args. 196 ;; before doing anything else we lock SELF, which should remain 197 ;; locked for the full runtime duration. 198 (setf (cli-lock-p self) t) 200 for (node . tail) on (debug! (ast ast)) 203 (let ((kind (cli-node-kind node)) (form (cli-node-form node))) 207 (let ((name (cli-opt-name form))) 208 (when-let ((o (car (find-opts self name)))) 210 (setf (cli-opt-lock o) t)))) 211 ;; when we encounter a command we recurse over the tail 213 (when-let ((c (find-cmd self (cli-name form)))) 214 ;; handle the rest of the AST 215 (setf c (install-ast c (make-cli-ast tail))) 216 (return-from install))) 217 (arg (push-arg form self))))) 218 (setf (cli-cmd-args self) (nreverse (cli-cmd-args self))) 221 (defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile) 222 "Install THUNK into the corresponding slot in cli-cmd SELF." 223 (let ((%thunk (if compile (compile nil lambda) lambda))) 224 (setf (cli-thunk self) %thunk) 227 (defmethod push-arg (arg (self cli-cmd)) 228 "Push an ARG onto the corresponding slot of a CLI-CMD." 229 (push arg (cli-cmd-args self))) 231 (defmethod parse-args ((self cli-cmd) args &key (compile t)) 232 "Parse ARGS and return the updated object SELF. 233 ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless COMPILE is 234 t, in which case a list of strings is assumed." 235 (with-slots (opts cmds) self 236 (let ((args (if compile (proc-args self args) args))) 237 (install-ast self args)))) 239 ;; WARNING: make sure to fill in the opt and cmd slots with values 240 ;; from the top-level args before calling a command. 241 (defmethod call-cmd ((self cli-cmd) args opts) 242 (trace! "calling command:" args opts) 243 (funcall (cli-thunk self) args opts)) 245 (defmethod do-cmd ((self cli-cmd)) 246 "Perform the command, recursively calling child commands and opts if necessary." 247 (loop for o across (active-opts self) 250 (call-cmd self (cli-cmd-args self) (active-opts self)) 251 (loop for c across (active-cmds self)