changelog shortlog graph tags branches changeset files revisions annotate raw help

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

changeset 683: c5fe76568de0
parent: 65102f74d1ae
child: 517c65b51e6b
author: Richard Westhaver <ellis@rwest.io>
date: Sat, 28 Sep 2024 17:48:06 -0400
permissions: -rw-r--r--
description: fixed clap objects to support make-load-form method - thunk is symbol only
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 opts :type (vector cli-opt))
17  (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd :adjustable t)
18  :accessor cmds :type (vector cli-cmd))
19  (thunk :initform 'default-thunk :initarg :thunk :accessor cli-thunk :type symbol)
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  self))
32 
33 (defmethod make-load-form ((obj cli-cmd) &optional env)
34  (make-load-form-saving-slots
35  obj
36  :slot-names '(name opts cmds thunk lock description args)
37  :environment env))
38 
39 (defmethod print-object ((self cli-cmd) stream)
40  (print-unreadable-object (self stream :type t)
41  (format stream "~A :opts ~A :cmds ~A :args ~A"
42  (cli-name self)
43  (length (opts self))
44  (length (cmds self))
45  (length (cli-cmd-args self)))))
46 
47 (defmethod print-usage ((self cli-cmd) &optional stream)
48  (with-slots (opts cmds) self
49  (format stream "~(~A~) ~A~A~A"
50  (cli-name self)
51  (if-let ((d (and (slot-boundp self 'description) (cli-description self))))
52  (format nil ": ~A" d)
53  "")
54  (if (null opts)
55  ""
56  (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil))))
57  (if (null cmds)
58  ""
59  (format nil "~{!~A~}" (loop for c across cmds collect (print-usage c nil)))))))
60 
61 (defmethod push-cmd ((self cli-cmd) (place cli-cmd))
62  (vector-push self (cmds place)))
63 
64 (defmethod push-opt ((self cli-opt) (place cli-cmd))
65  (vector-push self (opts place)))
66 
67 (defmethod pop-cmd ((self cli-cmd))
68  (vector-pop (cmds self)))
69 
70 (defmethod pop-opt ((self cli-opt))
71  (vector-pop (opts self)))
72 
73 (defmethod handle-unknown-opt ((self cli-cmd) (opt string))
74  (with-opt-restart-case opt
75  (clap-unknown-argument opt 'cli-opt)))
76 
77 (defmethod handle-invalid-opt ((self cli-cmd) (opt string) &optional reason)
78  (clap-invalid-argument opt :kind 'cli-opt :reason reason))
79 
80 (defmethod handle-missing-opt ((self cli-cmd) (opt string))
81  (clap-missing-argument opt 'cli-opt))
82 
83 (defmethod cli-equal ((a cli-cmd) (b cli-cmd))
84  (with-slots (name opts cmds) a
85  (with-slots ((bn name) (bo opts) (bc cmds)) b
86  (and (string= name bn)
87  (if (and (null opts) (null bo))
88  t
89  (unless (member nil (loop for oa across opts
90  for ob across bo
91  collect (cli-equal oa ob)))
92  t))
93  (if (and (null cmds) (null bc))
94  t
95  (unless (member nil (loop for ca across cmds
96  for cb across bc
97  collect (cli-equal ca cb)))
98  t))))))
99 
100 (defmethod find-cmd ((self cli-cmd) name &optional active)
101  (when-let ((c (find name (cmds self) :key #'cli-name :test #'string=)))
102  (if active
103  ;; maybe issue warning here? report to user
104  (when (cli-lock-p c)
105  c)
106  c)))
107 
108 (defmethod (setf find-cmd) ((new cli-cmd) (self cli-cmd) name &optional active)
109  (let ((match (find-cmd self name active) ))
110  (substitute new match (cmds self) :test 'cli-equal)))
111 
112 (defmethod active-cmds ((self cli-cmd))
113  (remove-if-not #'cli-lock-p (cmds self)))
114 
115 (defmethod activate-cmd ((self cli-cmd))
116  (setf (cli-lock-p self) t))
117 
118 (defmethod find-opts ((self cli-cmd) name &key active recurse)
119  (let ((ret))
120  (flet ((%find (o obj)
121  (when-let ((found (find o (opts obj) :key #'cli-opt-name :test 'equal)))
122  (push found ret))))
123  (when (and recurse (cmds self))
124  (loop for c across (cmds self)
125  do (%find name c)))
126  (%find name self)
127  (when active
128  (setf ret (remove-if-not #'cli-lock-p ret)))
129  ret)))
130 
131 (defmethod find-opt ((self cli-cmd) name &optional active)
132  (let ((ret (find name (opts self) :key #'cli-opt-name :test 'equal)))
133  (if active
134  (when (cli-opt-lock ret) ret)
135  ret)))
136 
137 (defmethod (setf find-opt) ((new cli-opt) (self cli-cmd) name &optional active)
138  (let ((match (find-opt self name active)))
139  (substitute new match (opts self) :test 'cli-equal)))
140 
141 (defmethod active-opts ((self cli-cmd) &optional global)
142  (remove-if-not
143  (if global
144  #'active-global-opt-p
145  #'cli-opt-lock)
146  (opts self)))
147 
148 (defmethod find-short-opts ((self cli-cmd) ch &key recurse)
149  (let ((ret))
150  (flet ((%find (ch obj)
151  (when-let ((found (find ch (opts obj) :key #'cli-opt-name :test #'opt-string-prefix-eq)))
152  (push found ret))))
153  (when (and recurse (cmds self))
154  (loop for c across (cmds self)
155  do (%find ch c)))
156  (%find ch self)
157  ret)))
158 
159 (declaim (inline solop))
160 (defun solop (self)
161  (= 0 (length (active-cmds self)) (length (active-opts self))))
162 
163 (defmacro with-opt-restart-case (arg condition)
164  "Bind restarts 'use-as-arg' and 'discard-arg' for duration of BODY."
165  `(restart-case ,condition
166  (use-as-arg () () (make-cli-node 'arg ,arg))
167  (discard-arg () () (setf ,arg nil))))
168 
169 (defmethod proc-args ((self cli-cmd) args)
170  "Process ARGS into an ast. Each element of the ast is a node with a
171 :kind slot, indicating the type of node and a :form slot which stores
172 an object."
173  (make-cli-ast
174  (loop
175  with skip
176  with exit
177  for (a . args) on args
178  if skip
179  do (setq skip nil)
180  else if exit
181  do (return)
182  ;; TODO 2024-09-15: handle flag groups -abcd
183  else if (short-opt-p a) ;; SHORT OPT
184  collect
185  (if-let ((o (car (find-short-opts self (aref a 1) :recurse nil))))
186  (%compose-short-opt o)
187  (with-opt-restart-case a
188  (clap-unknown-argument a 'cli-opt)))
189  else if (long-opt-p a) ;; LONG OPT
190  collect
191  (let* ((has-eq (long-opt-has-eq-p a))
192  (name (or (car has-eq) (string-left-trim "-" a)))
193  (o (car (find-opts self name :recurse nil))))
194  (cond
195  ((and has-eq o)
196  (setf (cli-opt-val o) (cdr has-eq))
197  (make-cli-node 'opt o))
198  ((and (not has-eq) o)
199  (prog1
200  (%compose-long-opt o (pop args))
201  (setq skip t)))
202  (t ;; (not o) (not has-eq)
203  (with-opt-restart-case a
204  (clap-unknown-argument a 'cli-opt)))))
205  ;; OPT GROUP
206  else if (opt-group-p a)
207  collect
208  (make-cli-node 'group nil)
209  ;; OPT KEYWORD (experimental)
210  else if (opt-keyword-p a)
211  collect (if-let ((o (car (find-opts self (string-left-trim ":" a) :recurse nil))))
212  (prog1 (%compose-keyword-opt o (pop args))
213  (setq exit t))
214  (make-cli-node 'arg a))
215  else ;; CMD or ARG
216  collect
217  (if-let ((cmd (find-cmd self a)))
218  (prog1 (make-cli-node 'cmd (parse-args cmd args :compile t))
219  (setq exit t))
220  ;; just a plain arg - move to next
221  (make-cli-node 'arg a)))))
222 
223 (defmethod install-ast ((self cli-cmd) (ast cli-ast))
224  "Install the given AST, recursively filling in value slots."
225  (with-slots (cmds opts) self
226  ;; we assume all nodes in the ast have been validated and the ast
227  ;; itself is consumed. validation is performed in proc-args.
228 
229  ;; before doing anything else we lock SELF, which should remain
230  ;; locked until all subcommands have completed
231  (activate-cmd self)
232  (loop named install
233  for (node . tail) on (ast ast)
234  while node
235  do
236  (let ((kind (cli-node-kind node))
237  (form (cli-node-form node)))
238  (case kind
239  ;; opts
240  (opt
241  (setf #1=(find-opt self (cli-name form)) form)
242  (activate-opt #1#)
243  (log:trace! (format nil "installing opt ~A" (cli-name form))))
244  (cmd
245  (setf (find-cmd self (cli-name form)) form)
246  (log:trace! (format nil "installing cmd ~A" (cli-name form))))
247  (arg (push-arg form self)))))
248  (setf (cli-cmd-args self) (nreverse (cli-cmd-args self)))
249  self))
250 
251 (defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile)
252  "Install THUNK into the corresponding slot in cli-cmd SELF."
253  (let ((%thunk (if compile (compile nil lambda) lambda)))
254  (setf (cli-thunk self) %thunk)
255  self))
256 
257 (defmethod push-arg (arg (self cli-cmd))
258  "Push an ARG onto the corresponding slot of a CLI-CMD."
259  (push arg (cli-cmd-args self)))
260 
261 (defmethod parse-args ((self cli-cmd) args &key (compile t))
262  "Parse ARGS and return the updated object SELF.
263 ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless COMPILE is
264 t, in which case a list of strings is assumed."
265  (with-slots (opts cmds) self
266  (let ((args (if compile (proc-args self args) args)))
267  (install-ast self args))))
268 
269 ;; WARNING: make sure to fill in the opt and cmd slots with values
270 ;; from the top-level args before calling a command.
271 (defmethod call-cmd ((self cli-cmd) args opts)
272  (trace! "calling command:" args opts)
273  (funcall (cli-thunk self) args opts))
274 
275 (defmethod do-opts ((self cli-cmd) &optional global)
276  (do-opts (active-opts self) global))
277 
278 (defmethod do-cmd ((self cli-cmd))
279  "Perform the active command or subcommand, recursively calling DO-CMD on
280 subcommands until a level is reached which satisfies SOLOP. active OPTS are
281 evaluated with DO-OPTS along the way."
282  (do-opts self)
283  (if (solop self)
284  (call-cmd self (cli-cmd-args self) (active-opts self))
285  (loop for c across (active-cmds self)
286  do (do-cmd c))))
287