changelog shortlog graph tags branches changeset files revisions annotate raw help

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

changeset 651: af486e0a40c9
parent: 74e563ed4537
child: 65102f74d1ae
author: Richard Westhaver <ellis@rwest.io>
date: Sat, 14 Sep 2024 22:13:06 -0400
permissions: -rw-r--r--
description: multi-binaries, working on removing x.lisp
1 ;;; cli/clap/cli.lisp --- Clap CLI Class
2 
3 ;; Top-level command object of a CLI App
4 
5 ;;; Code:
6 (in-package :cli/clap/obj)
7 
8 (defun make-cli (kind &rest slots)
9  "Creates a new CLI object of the given kind."
10  (declare (type (member :opt :cmd :cli t) kind))
11  (cond
12  ((eql kind :cli) (apply #'make-instance 'cli slots))
13  ((eql kind :opt) (apply #'make-cli-opt slots))
14  ((eql kind :cmd) (apply #'make-instance 'cli-cmd slots))
15  (t (apply #'make-instance kind slots))))
16 
17 (defmacro define-cli (sym &key name version description thunk opts cmds)
18  "Define a symbol NAME bound to a top-level CLI object."
19  (with-gensyms (%name %class)
20  (if (atom sym)
21  (setq %name sym
22  %class :cli)
23  (setq %name (car sym)
24  %class (cdr sym)))
25  `(,*default-cli-def* ,%name (make-cli ,%class :name ,name
26  :version ,version
27  :description ,description
28  :thunk ,thunk
29  :opts (make-opts ',opts)
30  :cmds (make-cmds ',cmds)))))
31 
32 (defmacro defmain (name (&key (exit t)) &body body)
33  "Define a CLI main function in the current package."
34  `(let ((*no-exit* ,(not exit)))
35  (defun ,name ()
36  "Run the top-level function and print to *STDOUT*."
37  (with-cli-handlers
38  (progn
39  ,@body)))))
40 
41 ;; RESEARCH 2023-09-12: closed over hash-table with short/long flags
42 ;; to avoid conflicts. if not, need something like a flag-function
43 ;; slot at class allocation.
44 (defun make-opts (opts)
45  "Make a vector of CLI-OPTs based on OPTS."
46  (map 'vector
47  (lambda (x)
48  (etypecase x
49  (string (make-cli-opt :name x))
50  (list (apply #'make-cli :opt x))
51  (t (make-cli :opt :name (format nil "~(~A~)" x) :global t))))
52  opts))
53 
54 (defun make-cmds (cmds)
55  "Make a vector of CLI-CMDs based on CMDS."
56  (map 'vector
57  (lambda (x)
58  (etypecase x
59  (cli-cmd x)
60  (string (make-cli :cmd :name x))
61  (list (apply #'make-cli :cmd x))
62  (t (make-cli :cmd :name (format nil "~(~A~)" x)))))
63  cmds))
64 
65 (defclass cli (cli-cmd)
66  ;; name slot defaults to *package*, must be string
67  ((name :initarg :name :initform (string-downcase (package-name *package*)) :accessor cli-name :type string)
68  (version :initarg :version :initform "0.1.0" :accessor cli-version :type string)
69  ;; TODO 2023-10-11: look into pushd popd - cd-stack?
70  (cd :initarg :cd :initform (sb-posix:getcwd) :type string :accessor cli-cd
71  :documentation "working directory of the top-level CLI."))
72  (:documentation "CLI"))
73 
74 (defmethod print-usage ((self cli) &optional stream)
75  (iprintln (format nil "usage: ~A [global] <command> [<arg>]~%" (cli-name self)) 2 stream))
76 
77 (defmethod print-version ((self cli) &optional stream)
78  (println (cli-version self) stream))
79 
80 (defmethod print-help ((self cli) &optional (stream t))
81  (println (format nil "~A v~A --- ~A~%" (cli-name self) (cli-version self) (cli-description self)) stream)
82  (print-usage self stream)
83  ;; (terpri stream)
84  (println "options:" stream)
85  (with-slots (opts cmds) self
86  (unless (null opts)
87  (loop for o across opts
88  do (iprintln (print-usage o nil) 2 stream)))
89  (terpri stream)
90  (println "commands:" stream)
91  (unless (null cmds)
92  (loop for c across cmds
93  do (iprintln (print-usage c nil) 2 stream)))))
94 
95 (defmethod cli-equal :before ((a cli) (b cli))
96  "Return T if A is the same cli object as B.
97 
98 Currently this function is intended only for instances of the CLI
99 class and is used as a specialized EQL for DEFINE-CONSTANT."
100  (with-slots (version) a
101  (with-slots ((bv version)) b
102  (string= version bv))))
103 
104 (declaim (inline debug-opts))
105 (defun debug-opts (cli)
106  (let ((o (active-opts cli))
107  (a (cli-cmd-args cli))
108  (c (active-cmds cli)))
109  (log:debug! :pwd (cli-cd cli) :active-opts o :cmd-args a :active-cmds c)))
110 
111 (defmethod do-opts ((self cli) &optional global)
112  (loop for opt across (active-opts self global)
113  do (do-opt opt)))
114 
115 (defmacro with-cli ((cli &rest slots) args &body body)
116  "Like with-slots with some extra bindings.
117 
118 SLOTS is a list passed to WITH-SLOTS.
119 
120 CLI is updated based on the current environment and dynamically bound
121 to *CLI*. ARGS is a list of CLI args, defaults to *POSIX-ARGV* at
122 runtime if nil."
123  `(progn
124  (let ((*cli* ,cli))
125  (setf (cli-cd *cli*) *default-pathname-defaults*)
126  (with-slots ,slots (parse-args *cli* ,args :compile t)
127  ,@body))))