changelog shortlog graph tags branches changeset files revisions annotate raw help

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

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