changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/cli/clap/cmd.lisp

changeset 645: 3e6a17fb5712
parent: f901de70a80e
child: 95fd920af398
author: Richard Westhaver <ellis@rwest.io>
date: Wed, 11 Sep 2024 17:24:07 -0400
permissions: -rw-r--r--
description: clap upgrades
1 ;;; cli/clap/cmd.lisp --- Clap Commands
2 
3 ;; Command Objects used to build CLI Applications.
4 
5 ;;; Commentary:
6 
7 ;;
8 
9 ;;; Code:
10 (in-package :cli/clap/obj)
11 
12 (defclass cli-cmd ()
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."))
25 
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)))
32  self))
33 
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"
37  (cli-name self)
38  (length (cli-opts self))
39  (length (cli-cmds self))
40  (length (cli-cmd-args self)))))
41 
42 (defmethod print-usage ((self cli-cmd) &optional stream)
43  (with-slots (opts cmds) self
44  (format stream "~(~A~) ~A~A~A"
45  (cli-name self)
46  (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
47  (format nil ": ~A" d)
48  "")
49  (if (null opts)
50  ""
51  (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil))))
52  (if (null cmds)
53  ""
54  (format nil "~{!~A~}" (loop for c across cmds collect (print-usage c nil)))))))
55 
56 (defmethod push-cmd ((self cli-cmd) (place cli-cmd))
57  (vector-push self (cli-cmds place)))
58 
59 (defmethod push-opt ((self cli-opt) (place cli-cmd))
60  (vector-push self (cli-opts place)))
61 
62 (defmethod pop-cmd ((self cli-cmd))
63  (vector-pop (cli-cmds self)))
64 
65 (defmethod pop-opt ((self cli-opt))
66  (vector-pop (cli-opts self)))
67 
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))
73  t
74  (unless (member nil (loop for oa across opts
75  for ob across bo
76  collect (cli-equal oa ob)))
77  t))
78  (if (and (null cmds) (null bc))
79  t
80  (unless (member nil (loop for ca across cmds
81  for cb across bc
82  collect (cli-equal ca cb)))
83  t))))))
84 
85 (defmethod find-cmd ((self cli-cmd) name &optional active)
86  (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=)))
87  (if active
88  ;; maybe issue warning here? report to user
89  (if (cli-lock-p c)
90  c
91  (clap-simple-error "inactive (unlocked) cmd: ~A" c))
92  c)))
93 
94 (defmethod active-cmds ((self cli-cmd))
95  (remove-if-not #'cli-lock-p (cli-cmds self)))
96 
97 (defmethod find-opts ((self cli-cmd) name &key active recurse)
98  (let ((ret))
99  (flet ((%find (o obj)
100  (when-let ((found (find o (cli-opts obj) :key #'cli-opt-name :test 'equal)))
101  (push found ret))))
102  (when (and recurse (cli-cmds self))
103  (loop for c across (cli-cmds self)
104  do (%find name c)))
105  (%find name self)
106  (when active
107  (setf ret (remove-if-not #'cli-lock-p ret)))
108  ret)))
109 
110 (defmethod active-opts ((self cli-cmd) &optional global)
111  (remove-if-not
112  (if global
113  #'active-global-opt-p
114  #'cli-opt-lock)
115  (cli-opts self)))
116 
117 (defmethod find-short-opts ((self cli-cmd) ch &key recurse)
118  (let ((ret))
119  (flet ((%find (ch obj)
120  (when-let ((found (find ch (cli-opts obj) :key #'cli-opt-name :test #'opt-string-prefix-eq)))
121  (push found ret))))
122  (when (and recurse (cli-cmds self))
123  (loop for c across (cli-cmds self)
124  do (%find ch c)))
125  (%find ch self)
126  ret)))
127 
128 (declaim (inline solop))
129 (defun solop (self)
130  (and (= 0 (length (active-cmds self)) (length (active-opts self)))))
131 
132 (defmacro with-opt-restart-case (arg condition)
133  "Bind restarts 'use-as-arg' and 'discard-arg' for duration of BODY."
134  `(restart-case ,condition
135  (use-as-arg () () (make-cli-node 'arg ,arg))
136  (discard-arg () () nil)))
137 
138 (defmethod proc-args ((self cli-cmd) args)
139  "Process ARGS into an ast. Each element of the ast is a node with a
140 :kind slot, indicating the type of node and a :form slot which stores
141 a value.
142 
143 For now we parse group separators '--' and insert a nil into the tree,
144 this will likely change to generating a new branch in the ast as it
145 should be."
146  (make-cli-ast
147  (let ((holes)) ;; list of arg indexes which can be skipped since they're
148  ;; consumed by an opt
149  (loop
150  for i below (length args)
151  for (a . args) on args
152  if (member i holes)
153  do (continue) ;; skip args which have been consumed already
154  ;; else
155  ;; if (= (length a) 1)
156  ;; collect (make-cli-node 'arg a) ; always treat single-char as arg
157  else
158  if (short-opt-p a) ;; SHORT OPT
159  collect
160  (if-let ((o (find-short-opts self (aref a 1) :recurse t)))
161  (%compose-short-opt (car o) a)
162  ;; TODO 2024-09-11: signal error?
163  (with-opt-restart-case a
164  (clap-unknown-argument a)))
165  else
166  if (long-opt-p a) ;; LONG OPT
167  collect
168  (let ((o (find-opts self (string-left-trim "-" a) :recurse t))
169  (has-eq (long-opt-has-eq-p a)))
170  (cond
171  ((and has-eq o)
172  (setf (cli-opt-val o) (cdr has-eq))
173  (make-cli-node 'opt o))
174  ((and (not has-eq) o)
175  (prog1 (%compose-long-opt (car o) args)
176  (push (1+ i) holes)))
177  (t ;; (not o) (not has-eq)
178  (with-opt-restart-case a
179  (clap-unknown-argument a)))))
180  ;; OPT GROUP
181  else
182  if (opt-group-p a)
183  collect nil
184  ;; CMD
185  else
186  collect
187  (let ((cmd (find-cmd self a)))
188  (if cmd
189  ;; TBD
190  (make-cli-node 'cmd (find-cmd self a))
191  ;; ARG
192  (make-cli-node 'arg a)))))))
193 
194 (defmethod install-ast ((self cli-cmd) (ast cli-ast))
195  "Install the given AST, recursively filling in value slots."
196  (with-slots (cmds opts) self
197  ;; we assume all nodes in the ast have been validated and the ast
198  ;; itself is consumed. validation is performed in proc-args.
199 
200  ;; before doing anything else we lock SELF, which should remain
201  ;; locked for the full runtime duration.
202  (setf (cli-lock-p self) t)
203  (loop named install
204  for (node . tail) on (debug! (ast ast))
205  until (null node)
206  do
207  (let ((kind (cli-node-kind node)) (form (cli-node-form node)))
208  (case kind
209  ;; opts
210  (opt
211  (let ((name (cli-opt-name form)))
212  (when-let ((o (car (find-opts self name))))
213  (setf o form)
214  (setf (cli-opt-lock o) t))))
215  ;; when we encounter a command we recurse over the tail
216  (cmd
217  (when-let ((c (find-cmd self (cli-name form))))
218  ;; handle the rest of the AST
219  (setf c (install-ast c (make-cli-ast tail)))
220  (return-from install)))
221  (arg (push-arg form self)))))
222  (setf (cli-cmd-args self) (nreverse (cli-cmd-args self)))
223  self))
224 
225 (defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile)
226  "Install THUNK into the corresponding slot in cli-cmd SELF."
227  (let ((%thunk (if compile (compile nil lambda) lambda)))
228  (setf (cli-thunk self) %thunk)
229  self))
230 
231 (defmethod push-arg (arg (self cli-cmd))
232  "Push an ARG onto the corresponding slot of a CLI-CMD."
233  (push arg (cli-cmd-args self)))
234 
235 (defmethod parse-args ((self cli-cmd) args &key (compile t))
236  "Parse ARGS and return the updated object SELF.
237 ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless COMPILE is
238 t, in which case a list of strings is assumed."
239  (with-slots (opts cmds) self
240  (let ((args (if compile (proc-args self args) args)))
241  (install-ast self args))))
242 
243 ;; WARNING: make sure to fill in the opt and cmd slots with values
244 ;; from the top-level args before calling a command.
245 (defmethod call-cmd ((self cli-cmd) args opts)
246  (trace! "calling command:" args opts)
247  (funcall (cli-thunk self) args opts))
248 
249 (defmethod do-cmd ((self cli-cmd))
250  "Perform the command, recursively calling child commands and opts if necessary."
251  (loop for o across (active-opts self)
252  do (do-opt o))
253  (if (solop self)
254  (call-cmd self (cli-cmd-args self) (active-opts self))
255  (loop for c across (active-cmds self)
256  do (do-cmd c))))
257