Mercurial > core / lisp/lib/cli/pkg.lisp
changeset 116: |
797d729e14cc |
parent: |
cae8da4b1415
|
child: |
d0b235557fab |
author: |
ellis <ellis@rwest.io> |
date: |
Thu, 21 Dec 2023 15:10:14 -0500 |
permissions: |
-rw-r--r-- |
description: |
added cli/ansi and did some work on ffi/xkb |
1 ;;; cli.lisp --- cli programming api and utils 3 ;; This package contains a simple api and macros for building lisp CLI 8 ;; - inspired by: clingon, uiop 10 ;; Basic assumptions at runtime: 11 ;; - running in a POSIX-compliant shell 12 ;; - output stream supports UTF-8 14 ;; TODO 2023-10-14: install-ast, install-thunk, proc-args, etc should 15 ;; return IR types - CLI-IR THUNK and CLI-IR respectively. 17 ;; TODO 2023-10-14: rename cli-ast to cli-ir, install-ast to 21 (uiop:define-package :cli 22 (:use :cl :std :log :sb-ext) 23 (:import-from :uiop :println) 24 (:import-from :sb-ext :parse-native-namestring) 25 (:shadowing-import-from :sb-ext :exit) 32 :*cli-group-separator* 82 :handle-unknown-argument 83 :handle-missing-argument 84 :handle-invalid-argument 113 (defpackage :cli/ansi 119 :ris :reset-to-initial-state 125 :cub :cursor-backward 126 :cnl :cursor-next-line 127 :cpl :cursor-preceding-line 128 :cha :cursor-horizontal-absolute 129 :cup :cursor-position 130 :vpa :vertical-position-absolute 131 :vpr :vertical-position-relative 132 :vpb :vertical-position-backward 133 :scosc :save-cursor-position 134 :scorc :restore-cursor-position 135 :ed :erase-in-display :erase-below :erase-above :erase :erase-saved-lines 136 :el :erase-in-line :erase-right :erase-left :erase-line 137 :sgr :select-graphic-rendition 138 :dsr :device-status-report 139 ;; DEC private mode set and reset 140 :decset :dec-private-mode-set 141 :decrst :dec-private-mode-reset 142 :show-cursor :hide-cursor 143 :use-alternate-screen-buffer :use-normal-screen-buffer 150 (defpackage :cli/progress 156 :*progress-bar-enabled* 157 :start-progress-display 158 :finish-progress-display 160 :uncertain-size-progress-bar 163 (defpackage :cli/spark 169 (defpackage :cli/repl 170 (:use :cl :std :cli :cli/progress :cli/spark) 177 (defun cli-arg0 () (car sb-ext:*posix-argv*)) 178 (defun cli-args () (cdr sb-ext:*posix-argv*)) 180 (declaim (inline exec-path-list)) 181 (defun exec-path-list () 182 "Return a list of all members of PATH" 183 (let ((var (sb-posix:getenv "PATH"))) 184 (let ((lst (loop for i = 0 then (1+ j) 185 as j = (position #\: var :start i) 186 when (uiop:directory-exists-p (probe-file (subseq var i j))) 187 collect (probe-file (subseq var i j)) 189 (unless (null (car lst)) 190 (mapcar (lambda (x) (car (directory x))) 193 (defun program-list () 194 "Return a fresh list of all files in PATH directories." 195 (loop for p in (exec-path-list) 196 append (uiop:directory-files p))) 198 (defun find-exe (name &optional programs) 199 "Find NAME in list of PROGRAMS, defaulting to the result of #'program-list." 200 (find name (or programs (program-list)) 202 :key #'pathname-name)) 204 (defun open-with-editor (path &key editor args) 205 (unless editor (setq editor (or (sb-posix:getenv "EDITOR") (find-exe "emacs")))) 206 (run-program editor (nconc args (list path)))) 208 (declaim (inline ld-library-path-list)) 209 (defun ld-library-path-list () 210 (let ((var (sb-posix:getenv "LD_LIBRARY_PATH"))) 211 (let ((lst (loop for i = 0 then (1+ j) 212 as j = (position #\: var :start i) 213 collect (subseq var i j) 215 (unless (null (car lst)) 216 (mapcar (lambda (x) (car (directory x))) lst))))) 218 (defparameter *cli-group-separator* 220 "A marker specifying the end of a unique group of CLI args.") 222 ;; uiop:command-line-arguments 225 (defmacro argp (arg &optional (args (cli-args))) 226 "Test for presence of ARG in ARGS. Return the tail of 227 ARGS starting from the position of ARG." 228 `(member ,arg ,args :test #'string=)) 230 (defmacro make-shorty (name) 231 "Return the first char of symbol or string NAME." 232 `(character (aref (if (stringp ,name) ,name (symbol-name ,name)) 0))) 234 ;; (defun treat-as-argument (condition) 235 ;; "A handler which can be used to invoke the `treat-as-argument' restart" 236 ;; (invoke-restart (find-restart 'treat-as-argument condition))) 238 ;; (defun discard-argument (condition) 239 ;; "A handler which can be used to invoke the `discard-argument' restart" 240 ;; (invoke-restart (find-restart 'discard-argument condition))) 242 (defmacro with-cli-handlers (form) 243 "A wrapper which handles common cli errors that may occur during 246 (sb-sys:interactive-interrupt () 247 (format *error-output* "~&(:SIGINT)~&") 250 (format *error-output* "~&~A~&" c) 253 (defun init-args () (setq *argv* (cons (cli-arg0) (cli-args)))) 255 (defmacro with-cli (slots cli &body body) 256 "Like with-slots with some extra bindings." 257 ;; (with-gensyms (cli-body) 258 ;; (let ((cli-body (mapcar (lambda (x) ()) cli-body) 261 (setf (cli-cwd ,cli) (sb-posix:getcwd)) 262 (with-slots ,slots (parse-args ,cli *argv* :compile t) 266 (declaim (inline completing-read)) 267 (defun completing-read (prompt collection 268 &key (history nil) (default nil) 270 (input *standard-input*) 271 (output *standard-output*)) 273 "A simplified COMPLETING-READ for common-lisp. 275 The Emacs completion framework includes a function called 276 `completing-read' which prompts the user for input from the 277 mini-buffer. It is a very flexible interface which can be used to read 278 user input programatically. This is incredibly useful for building 279 data entry interfaces -- for example see the `defprompt' macro. 281 Obviously writing a completion framework is out-of-scope, but we can 282 simulate one by embedding a DSL in our prompters if we choose. For 283 example, perhaps we treat a single '?' character as a request from the 284 user to list valid options while continue waiting for input." 285 (princ prompt output) 286 ;; ensure we empty internal buffer 287 (finish-output output) 288 (let* ((coll collection) 289 (res (let ((%i (read-line input))) 290 (if (> (length %i) 0) 293 (find res coll :key key :test test) 297 (setf history (push r history))))) 299 (defmacro defprompt (var &optional prompt) 300 "Generate a 'prompter' from list or variable VAR and optional 303 This isn't an ideal solution as it does in fact expose a dynamic 304 variable (VAR-prompt-history). We should generate accessors and 305 keep the variables within lexical scope of the generated 307 (with-gensyms (s p h) 308 `(let ((,s (if (boundp ',var) (symbol-value ',var) 310 (defvar ,(symbolicate var) nil) 311 ',(symbolicate var)))) 312 (,p (when (stringp ,prompt) ,prompt)) ;; prompt string 313 (,h ',(symbolicate var '-prompt-history))) ;; history symbol 314 (defvar ,(symbolicate var '-prompt-history) nil) 315 (defun ,(symbolicate var '-prompt) () 316 ,(format nil "Prompt for a value from `~A', use DEFAULT if non-nil 317 and no value is provided by user, otherwise fallback to the `car' 318 of `~A-PROMPT-HISTORY'." var var) 320 (format nil "~A [~A]: " 322 (car (symbol-value ,h))) 323 ,s :history ,h :default nil))))) 325 (defmacro define-cli-constant (name cli &optional doc) 326 `(define-constant ,name ,cli ,@doc :test #'cli-equal)) 328 (defvar *default-cli-def* 'defparameter) 330 (defmacro defcmd (name &body body) 331 `(defun ,name ($args $opts) 332 (declare (ignorable $args $opts)) 333 (let (($argc (length $args)) 334 ($optc (length $opts))) 335 (declare (ignorable $argc $optc)) 338 (defmacro defopt (name &body body) 340 (declare (ignorable $val)) 343 (declaim (inline walk-cli-slots)) 344 (defun walk-cli-slots (cli) 345 "Walk the plist CLI, performing actions as necessary based on the slot 347 (loop for kv in (group cli 2) 348 when (eql :thunk (car kv)) 349 return (let ((th (cdr kv))) 350 (if (or (functionp th) (symbolp th)) (funcall th) (compile nil (lambda () th))))) 353 (defmacro define-cli (name &body body) 354 "Define a symbol NAME bound to a top-level CLI object." 355 (declare (type symbol name)) 356 `(,*default-cli-def* ,name (apply #'make-cli t (walk-cli-slots ',body)))) 358 (defmacro defmain (ret &body body) 359 "Define a CLI main function in the current package which returns RET. 361 Note that this macro does not export the defined function and requires 362 `cli:main' to be an external symbol." 364 (declaim (type stream output)) 365 (defun main (&key (output *standard-output*)) 366 "Run the top-level function and print to OUTPUT." 367 (let ((*standard-output* output)) 369 (progn ,@body ,ret)))))) 372 (defun make-cli (kind &rest slots) 373 "Creates a new CLI object of the given kind." 374 (declare (type (member :opt :cmd :cli t) kind)) 375 (apply #'make-instance 377 ((eql kind :cli) 'cli) 378 ((eql kind :opt) 'cli-opt) 379 ((eql kind :cmd) 'cli-cmd) 383 ;; RESEARCH 2023-09-12: closed over hash-table with short/long flags 384 ;; to avoid conflicts. if not, need something like a flag-function 385 ;; slot at class allocation. 386 (defmacro make-opts (&body opts) 390 (string (make-cli :opt :name x)) 391 (list (apply #'make-cli :opt x)) 392 (t (make-cli :opt :name (format nil "~(~A~)" x) :global t)))) 393 (walk-cli-slots ',opts))) 395 (defmacro make-cmds (&body opts) 399 (string (make-cli :cmd :name x)) 400 (list (apply #'make-cli :cmd x)) 401 (t (make-cli :cmd :name (format nil "~(~A~)" x))))) 402 (walk-cli-slots ',opts))) 404 (defun long-opt-p (str) 405 (and (char= (aref str 0) (aref str 1) #\-) 408 (defun short-opt-p (str) 409 (and (char= (aref str 0) #\-) 410 (not (char= (aref str 1) #\-)) 413 (defun opt-group-p (str) 414 (string= str *cli-group-separator*)) 416 (defun opt-prefix-eq (ch str) 417 (char= (aref str 0) ch)) 419 (defun gen-thunk-ll (origin args) 420 (let ((a0 (list (symb '$a 0) origin))) 422 (nconc (loop for i from 1 for a in args nconc (list (symb '$a i) a)) a0 ) 426 ;; (defmacro gen-cli-thunk (pvars &rest thunk) 427 ;; "Generate and return a function based on THUNK suitable for the :thunk 428 ;; slot of cli objects with pandoric bindings PVARS.") 431 (defgeneric push-cmd (cmd place)) 433 (defgeneric push-opt (opt place)) 435 (defgeneric pop-cmd (place)) 437 (defgeneric pop-opt (place)) 439 (defgeneric find-cmd (self name &optional active)) 441 (defgeneric find-opt (self name &optional active)) 443 (defgeneric active-cmds (self)) 445 (defgeneric active-opts (self &optional global)) 447 (defgeneric find-short-opt (self ch)) 449 (defgeneric call-opt (self arg)) 451 (defgeneric do-opt (self)) 453 (defgeneric call-cmd (self args opts)) 455 (defgeneric parse-args (self args &key &allow-other-keys) 456 (:documentation "Parse list of strings ARGS using SELF. 458 A list of the same length as ARGS is returned containing 'cli-ast' 459 objects: (OPT . (or char string)) (CMD . string) NIL")) 461 (defgeneric do-cmd (self) 462 (:documentation "Run the command SELF with args parsed at runtime.")) 464 (defgeneric print-help (self &optional stream) 465 (:documentation "Format cli SELF as a helpful string.")) 467 (defgeneric print-version (self &optional stream) 468 (:documentation "Print the version of SELF.")) 470 (defgeneric print-usage (self &optional stream) 471 (:documentation "Format cli SELF as a useful string.")) 473 (defgeneric handle-unknown-argument (self arg) 474 (:documentation "Handle an unknown argument.")) 476 (defgeneric handle-missing-argument (self arg) 477 (:documentation "Handle a missing argument.")) 479 (defgeneric handle-invalid-argument (self arg) 480 (:documentation "Handle an invalid argument.")) 482 (defgeneric cli-equal (a b)) 484 (defun default-thunk (cli) (lambda (x) (declare (ignore x)) (print-help cli))) 486 (defvar *cli-opt-kinds* '(bool str form list sym key num file dir)) 488 (defun cli-opt-kind-p (s) 489 (declare (type symbol s)) 490 (find s *cli-opt-kinds*)) 494 (defmacro make-opt-parser (kind-spec &body body) 495 "Return a KIND-opt-parser function based on KIND-SPEC which is either a 496 symbol from *cli-opt-kinds* or a list, and optional BODY which 497 is a list of handlers for the opt-val." 498 (let* ((kind (if (consp kind-spec) (car kind-spec) kind-spec)) 499 (super (when (consp kind-spec) (cadr kind-spec))) 500 (fn-name (symbolicate 'parse- kind '-opt))) 502 (let ((fn1 (when (not (eql 'nil super)) (symbolicate 'parse- super '-opt)))) 504 (defun ,fn-name ($val) 505 "Parse the cli-opt-val $VAL." 507 (when (not (eql ',fn1 'nil)) (setq $val (funcall ',fn1 $val))) 510 (make-opt-parser bool $val) 512 (make-opt-parser (str bool) (when (stringp $val) $val)) 514 (make-opt-parser (form str) (read-from-string $val)) 516 (make-opt-parser (list form) (when (listp $val) $val)) 518 (make-opt-parser (sym form) (when (symbolp $val) $val)) 520 (make-opt-parser (key form) (when (keywordp $val) $val)) 522 (make-opt-parser (num form) (when (numberp $val) $val)) 524 (make-opt-parser (file str) 525 (when $val (parse-native-namestring $val nil *default-pathname-defaults* :as-directory nil))) 527 (make-opt-parser (dir str) 528 (when $val (sb-ext:parse-native-namestring $val nil *default-pathname-defaults* :as-directory t)))) 532 ;; note that cli-opts can have a nil or unbound name slot 533 ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string) 534 (kind :initarg :kind :initform 'boolean :accessor cli-opt-kind :type cli-opt-kind-p) 535 (thunk :initform #'default-thunk :initarg :thunk :type function-lambda-expression :accessor cli-thunk) 536 (val :initarg :val :initform nil :accessor cli-val :type form) 537 (global :initarg :global :initform nil :accessor global-opt-p :type boolean) 538 (description :initarg :description :accessor cli-description :type string) 539 (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean)) 540 (:documentation "CLI option")) 542 (defmethod handle-unknown-argument ((self cli-opt) arg)) 543 (defmethod handle-missing-argument ((self cli-opt) arg)) 544 (defmethod handle-invalid-argument ((self cli-opt) arg)) 546 (defmethod initialize-instance :after ((self cli-opt) &key) 547 (with-slots (name thunk) self 548 (unless (stringp name) (setf name (format nil "~(~A~)" name))) 549 (when (symbolp thunk) (setf thunk (funcall (compile nil `(lambda () ,(symbol-function thunk)))))) 552 (defmethod install-thunk ((self cli-opt) (lambda function) &optional compile) 553 "Install THUNK into the corresponding slot in cli-cmd SELF." 554 (let ((%thunk (if compile (compile nil lambda) lambda))) 555 (setf (cli-thunk self) %thunk) 558 (defmethod print-object ((self cli-opt) stream) 559 (print-unreadable-object (self stream :type t) 560 (format stream "~A :global ~A :val ~A" 565 (defmethod print-usage ((self cli-opt) &optional stream) 566 (format stream " -~(~{~A~^/--~}~)~A~A" 567 (if-let ((n (cli-name self))) 568 (list (make-shorty n) n) 570 (if (global-opt-p self) "* " " ") 571 (if-let ((d (and (slot-boundp self 'description) (cli-description self)))) 572 (format stream ": ~A" d) 575 (defmethod cli-equal ((a cli-opt) (b cli-opt)) 576 (with-slots (name global kind) a 577 (with-slots ((bn name) (bg global) (bk kind)) b 578 (and (string= name bn) 582 (defmethod call-opt ((self cli-opt) arg) 583 (funcall (compile nil (cli-thunk self)) arg)) 585 (defmethod do-opt ((self cli-opt)) 586 (call-opt self (cli-val self))) 589 ;; name slot is required and must be a string 590 ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string) 591 (opts :initarg :opts :initform (make-array 0 :element-type 'cli-opt) 592 :accessor cli-opts :type (vector cli-opt)) 593 (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd) 594 :accessor cli-cmds :type (vector cli-cmd)) 595 (thunk :initform #'default-thunk :initarg :thunk :accessor cli-thunk :type function-lambda-expression) 596 (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean) 597 (description :initarg :description :accessor cli-description :type string) 598 (args :initform nil :initarg :args :accessor cli-cmd-args)) 599 (:documentation "CLI command")) 601 (defmethod initialize-instance :after ((self cli-cmd) &key) 602 (with-slots (name cmds opts thunk) self 603 (unless (stringp name) (setf name (format nil "~(~A~)" name))) 604 (unless (vectorp cmds) (setf cmds (funcall (compile nil `(lambda () ,cmds))))) 605 (unless (vectorp opts) (setf opts (funcall (compile nil `(lambda () ,opts))))) 606 (when (symbolp thunk) (setf thunk (symbol-function thunk))) 609 (defmethod print-object ((self cli-cmd) stream) 610 (print-unreadable-object (self stream :type t) 611 (format stream "~A :opts ~A :cmds ~A :args ~A" 613 (length (cli-opts self)) 614 (length (cli-cmds self)) 615 (length (cli-cmd-args self))))) 617 (defmethod print-usage ((self cli-cmd) &optional stream) 618 (with-slots (opts cmds) self 619 (format stream "~(~A~) ~A~A~A" 621 (if-let ((d (and (slot-boundp self 'description) (cli-description self)))) 622 (format nil ": ~A" d) 626 (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil)))) 629 (format nil "~% ~{! ~A~}" (loop for c across cmds collect (print-usage c nil))))))) 631 (defmethod push-cmd ((self cli-cmd) (place cli-cmd)) 632 (vector-push self (cli-cmds place))) 634 (defmethod push-opt ((self cli-opt) (place cli-cmd)) 635 (vector-push self (cli-opts place))) 637 (defmethod pop-cmd ((self cli-cmd)) 638 (vector-pop (cli-cmds self))) 640 (defmethod pop-opt ((self cli-opt)) 641 (vector-pop (cli-opts self))) 643 (defmethod cli-equal ((a cli-cmd) (b cli-cmd)) 644 (with-slots (name opts cmds) a 645 (with-slots ((bn name) (bo opts) (bc cmds)) b 646 (and (string= name bn) 647 (if (and (null opts) (null bo)) 649 (unless (member nil (loop for oa across opts 651 collect (cli-equal oa ob))) 653 (if (and (null cmds) (null bc)) 655 (unless (member nil (loop for ca across cmds 657 collect (cli-equal ca cb))) 660 ;; typically when starting from a top-level `cli', the global 661 ;; `cli-opts' will be parsed first, followed by the first command 662 ;; found. If a command is found, the tail of the list is passed as 663 ;; arguments to this function, which can pass additonal arguments to 666 ;; TODO 2023-09-12: Parsing restarts at the `*cli-group-separator*' 667 ;; if present, or stops at EOI. 669 (declaim (inline %make-cli-node)) 670 (defstruct (cli-node (:constructor %make-cli-node)) kind form) 672 (defun make-cli-node (kind form) 673 (%make-cli-node :kind kind :form form)) 675 (declaim (inline %make-cli-ast)) 676 (defstruct (cli-ast (:constructor %make-cli-ast)) ast) 678 (defun make-cli-ast (nodes) 679 (%make-cli-ast :ast nodes)) 681 (defmethod find-cmd ((self cli-cmd) name &optional active) 682 (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=))) 684 ;; maybe issue warning here? report to user 685 (when (cli-lock-p c) c) 688 (defmethod active-cmds ((self cli-cmd)) 689 (remove-if-not #'cli-lock-p (cli-cmds self))) 692 (defmethod find-opt ((self cli-cmd) name &optional active) 693 (when-let ((o (find name (cli-opts self) :key #'cli-name :test #'string=))) 695 (when (cli-lock-p o) o) 698 (defun active-global-opt-p (opt) 699 "Return non-nil if OPT is active at runtime and global." 700 (when (and (cli-lock-p opt) (global-opt-p opt)) t)) 702 (defmethod active-opts ((self cli-cmd) &optional global) 705 #'active-global-opt-p 709 (defmethod find-short-opt ((self cli-cmd) ch) 710 (find ch (cli-opts self) :key #'cli-name :test #'opt-prefix-eq)) 712 (defmethod proc-args ((self cli-cmd) args) 713 "process ARGS into an ast. Each element of the ast is a node with a 714 :kind slot, indicating the type of node and a :form slot which stores 717 For now we parse group separators '--' and insert a nil into the tree, 718 this will likely change to generating a new branch in the ast as it 723 if (= (length a) 1) collect (make-cli-node 'arg a) 725 else if (short-opt-p a) 726 collect (if-let ((o (find-short-opt self (aref a 1)))) 729 (make-cli-node 'opt o)) 730 (make-cli-node 'arg a)) 733 else if (long-opt-p a) 734 ;; what we actually want to do is consume the next sequence of args - TBD 735 collect (if-let ((o (find-opt self (string-trim "-" a)))) 737 (setf (cli-val o) (string-trim "-" a)) 738 (make-cli-node 'opt o)) 739 (make-cli-node 'arg a)) 741 else if (opt-group-p a) 744 else if (find-cmd self a) 746 collect (make-cli-node 'cmd (find-cmd self a)) 748 else collect (make-cli-node 'arg a)))) 750 (defmethod install-ast ((self cli-cmd) (ast cli-ast)) 751 "Install the given AST, recursively filling in value slots." 752 (with-slots (cmds opts) self 753 ;; we assume all nodes in the ast have been validated and the ast 754 ;; itself is consumed. validation is performed in proc-args. 756 ;; before doing anything else we lock SELF, which should remain 757 ;; locked for the full runtime duration. 758 (setf (cli-lock-p self) t) 760 for (node . tail) on (cli-ast-ast ast) 763 (with-slots (kind form) node 767 (let ((name (cli-name form)) 768 (val (cli-val form))) 769 (when-let ((o (find-opt self name))) 770 (setf (cli-val o) val 772 ;; when we encounter a command we recurse over the tail 774 (when-let ((c (find-cmd self (cli-name form)))) 775 (setf (cli-lock-p c) t) 776 ;; handle the rest of the AST 777 (install-ast c (make-cli-ast tail)) 778 (return-from install))) 779 (arg (push-arg form self))))) 780 (setf (cli-cmd-args self) (nreverse (cli-cmd-args self))) 783 (defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile) 784 "Install THUNK into the corresponding slot in cli-cmd SELF." 785 (let ((%thunk (if compile (compile nil lambda) lambda))) 786 (setf (cli-thunk self) %thunk) 789 (defmethod push-arg (arg (self cli-cmd)) 790 (push arg (cli-cmd-args self))) 792 (defmethod parse-args ((self cli-cmd) args &key (compile nil)) 793 "Parse ARGS and return the updated object SELF. 795 ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless 796 COMPILE is t, in which case a list of strings is assumed." 797 (with-slots (opts cmds) self 798 (let ((args (if compile (proc-args self args) args))) 799 (print (install-ast self args))))) 801 ;; warning: make sure to fill in the opt and cmd slots with values 802 ;; from the top-level args before doing a command. 803 (defmethod call-cmd ((self cli-cmd) args opts) 804 ;; TODO 2023-09-12: handle args/env 805 (funcall (cli-thunk self) args opts)) 807 (defmethod do-cmd ((self cli-cmd)) 808 (call-cmd self (cli-cmd-args self) (cli-opts self))) 810 (defclass cli (cli-cmd) 811 ;; name slot defaults to *package*, must be string 812 ((name :initarg :name :initform (string-downcase (package-name *package*)) :accessor cli-name :type string) 813 (version :initarg :version :initform "0.1.0" :accessor cli-version :type string) 814 ;; TODO 2023-10-11: look into pushd popd - wd-stack? 815 (cwd :initarg :cwd :initform (sb-posix:getcwd) :type string :accessor cli-cwd 816 :documentation "working directory of the top-level CLI.")) 817 (:documentation "CLI")) 819 (defmethod print-usage ((self cli) &optional stream) 820 (iprintln (format nil "usage: ~A [global] <command> [<arg>]~%" (cli-name self)) 2 stream)) 822 (defmethod print-version ((self cli) &optional stream) 823 (println (cli-version self) stream)) 825 (defmethod print-help ((self cli) &optional stream) 826 (println (format nil "~A v~A" (cli-name self) (cli-version self)) stream) 827 (print-usage self stream) 828 (iprintln (cli-description self) 2 stream) 830 (iprintln "options:" 2 stream) 831 (with-slots (opts cmds) self 833 (loop for o across opts 834 do (iprintln (print-usage o) 4 stream))) 836 (iprintln "commands:" 2 stream) 838 (loop for c across cmds 839 do (iprintln (print-usage c) 4 stream))))) 841 (defmethod cli-equal :before ((a cli) (b cli)) 842 "Return T if A is the same cli object as B. 844 Currently this function is intended only for instances of the CLI 845 class and is used as a specialized EQL for DEFINE-CONSTANT." 846 (with-slots (version) a 847 (with-slots ((bv version)) b 848 (string= version bv)))) 850 ;; same as cli-cmd method, default is to compile though 851 (defmethod parse-args ((self cli) (args list) &key (compile t)) 852 "Parse list of string arguments ARGS and return the updated object SELF." 853 (with-slots (opts cmds) self 854 (let ((args (if compile (proc-args self args) args))) 855 (install-ast self args)))) 857 (declaim (inline debug-opts)) 858 (defun debug-opts (cli) 859 (let ((o (active-opts cli)) 860 (a (cli-cmd-args cli)) 861 (c (active-cmds cli))) 862 (debug! (cli-cwd cli) o a c))) 864 (declaim (inline solop)) 866 (and (= 0 (length (active-opts self t)) (length (active-cmds self))))) 868 (defmethod do-cmd ((self cli)) 870 (call-cmd self (cli-cmd-args self) (cli-opts self)) 872 (loop for o across (active-opts self t) 874 (loop for c across (active-cmds self)