Mercurial > core / lisp/lib/cli/clap.lisp
changeset 357: |
7c1383c08493 |
parent: |
8f1c1d79a96c
|
child: |
a5a2d756ee2f |
author: |
Richard Westhaver <ellis@rwest.io> |
date: |
Tue, 21 May 2024 22:20:29 -0400 |
permissions: |
-rw-r--r-- |
description: |
port xsubseq, proc-parse. work on http and clap |
1 ;;; lib/cli/api.lisp --- Command Line Argument Parser 7 (declaim (optimize (speed 3))) 8 (defun cli-arg0 () (car sb-ext:*posix-argv*)) 9 (defun cli-args () (cdr sb-ext:*posix-argv*)) 11 (declaim (simple-string *cli-group-separator*)) 12 (defparameter *cli-group-separator* 14 "A marker specifying the end of a unique group of CLI args.") 16 ;; uiop:command-line-arguments 19 (defmacro argp (arg &optional (args (cli-args))) 20 "Test for presence of ARG in ARGS. Return the tail of 21 ARGS starting from the position of ARG." 22 `(member ,arg ,args :test 'equal)) 24 (defmacro make-shorty (name) 25 "Return the first char of symbol or string NAME." 26 `(character (aref (if (stringp ,name) ,name (symbol-name ,name)) 0))) 28 ;; (defun treat-as-argument (condition) 29 ;; "A handler which can be used to invoke the `treat-as-argument' restart" 30 ;; (invoke-restart (find-restart 'treat-as-argument condition))) 32 ;; (defun discard-argument (condition) 33 ;; "A handler which can be used to invoke the `discard-argument' restart" 34 ;; (invoke-restart (find-restart 'discard-argument condition))) 35 (deferror clap-error (std-error) () (:auto t)) 38 "Indicate whether the WITH-CLI-HANDLERS form should exit on completion.") 40 (defmacro with-cli-handlers (form) 41 "A wrapper which handles common cli errors that may occur during 45 (sb-ext:enable-debugger) 46 (sb-ext:disable-debugger)) 48 (sb-sys:interactive-interrupt () 50 (sb-ext:exit :code 130)) 51 ;; ,@(when *no-exit* '())) 54 (defmacro with-cli (slots cli &body body) 55 "Like with-slots with some extra bindings." 57 (setf (cli-cd ,cli) (sb-posix:getcwd)) 58 (with-slots ,slots (parse-args ,cli (cli-args) :compile t) 61 (defvar *default-cli-def* 'defparameter) 63 (defvar *default-cli-class* 'cli 64 "The name of the class of the top-level CLI object which will be 65 generated by the DEFINE-CLI macro.") 67 (defmacro defcmd (name &body body) 68 `(defun ,name ($args $opts) 69 (declare (ignorable $args $opts)) 70 (let (($argc (length $args)) 71 ($optc (length $opts))) 72 (declare (ignorable $argc $optc)) 75 (defmacro defopt (name &body body) 76 `(defun ,name (&optional $val) 77 (declare (ignorable $val)) 80 (declaim (inline walk-cli-slots)) 81 (defun walk-cli-slots (cli) 82 "Walk the plist CLI, performing actions as necessary based on the slot 84 (loop for kv in (group cli 2) 85 when (eql :thunk (car kv)) 86 return (let ((th (cdr kv))) 87 (if (or (functionp th) (symbolp th)) (funcall th) (compile nil (lambda () th))))) 90 (defmacro define-cli (name &body body) 91 "Define a symbol NAME bound to a top-level CLI object." 92 (with-gensyms (%name %class) 96 (setq %name (car name) 98 `(,*default-cli-def* ,%name (apply #'make-cli ,%class (walk-cli-slots ',body))))) 100 (defmacro defmain ((&key return (exit t)) &body body) 101 "Define a CLI main function in the current package." 102 (with-gensyms (retval) 103 (let ((main (symbolicate 'main))) 104 (when return (setf retval return)) 107 "Run the top-level function and print to *STDOUT*." 108 (let ((*no-exit* ,(not exit))) 110 (progn ,@body ,@(unless (not (boundp 'retval)) (list retval)))))) 111 (export '(,main)))))) 114 (defun make-cli (kind &rest slots) 115 "Creates a new CLI object of the given kind." 116 (declare (type (member :opt :cmd :cli t) kind)) 118 ((eql kind :cli) (apply #'make-instance 'cli slots)) 119 ((eql kind :opt) (apply #'make-cli-opt slots)) 120 ((eql kind :cmd) (apply #'make-instance 'cli-cmd slots)) 121 (t (apply #'make-instance kind slots)))) 123 ;; RESEARCH 2023-09-12: closed over hash-table with short/long flags 124 ;; to avoid conflicts. if not, need something like a flag-function 125 ;; slot at class allocation. 126 (defmacro make-opts (&body opts) 127 "Make a vector of CLI-OPTs based on OPTS." 131 (string (make-cli-opt :name x)) 132 (list (apply #'make-cli :opt x)) 133 (t (make-cli :opt :name (format nil "~(~A~)" x) :global t)))) 134 (walk-cli-slots ',opts))) 136 (defmacro make-cmds (&body cmds) 137 "Make a vector of CLI-CMDs based on CMDS." 141 (string (make-cli :cmd :name x)) 142 (list (apply #'make-cli :cmd x)) 143 (t (make-cli :cmd :name (format nil "~(~A~)" x))))) 144 (walk-cli-slots ',cmds))) 146 (defun long-opt-p (str) 147 (declare (simple-string str)) 148 (and (char= (aref str 0) (aref str 1) #\-) 151 (defun short-opt-p (str) 152 (declare (simple-string str)) 153 (and (char= (aref str 0) #\-) 154 (not (char= (aref str 1) #\-)) 157 (defun opt-group-p (str) 158 (declare (simple-string str)) 159 (equalp str *cli-group-separator*)) 161 (defun opt-string-prefix-eq (ch str) 162 (declare (simple-string str) (character ch)) 163 (char= ch (aref str 0))) 165 ;; currently not in use 166 (defun gen-thunk-ll (origin args) 167 (let ((a0 (list (symbolicate '$a 0) origin))) 169 (nconc (loop for i from 1 for a in args nconc (list (symbolicate '$a (the fixnum i)) a)) a0) 173 ;; (defmacro gen-cli-thunk (pvars &rest thunk) 174 ;; "Generate and return a function based on THUNK suitable for the :thunk 175 ;; slot of cli objects with pandoric bindings PVARS.") 178 (defgeneric push-cmd (cmd place)) 180 (defgeneric push-opt (opt place)) 182 (defgeneric pop-cmd (place)) 184 (defgeneric pop-opt (place)) 186 (defgeneric find-cmd (self name &optional active)) 188 (defgeneric find-opts (self name &key active recurse)) 190 (defgeneric active-cmds (self)) 192 (defgeneric active-opts (self &optional global)) 194 (defgeneric find-short-opts (self ch &key)) 196 (defgeneric call-opt (self arg)) 198 (defgeneric do-opt (self)) 200 (defgeneric call-cmd (self args opts)) 202 (defgeneric parse-args (self args &key &allow-other-keys) 203 (:documentation "Parse list of strings ARGS using SELF. 205 A list of the same length as ARGS is returned containing 'cli-ast' 206 objects: (OPT . (or char string)) (CMD . string) NIL")) 208 (defgeneric do-cmd (self) 209 (:documentation "Run the command SELF with args parsed at runtime.")) 211 (defgeneric print-help (self &optional stream) 212 (:documentation "Format cli SELF as a helpful string.")) 214 (defgeneric print-version (self &optional stream) 215 (:documentation "Print the version of SELF.")) 217 (defgeneric print-usage (self &optional stream) 218 (:documentation "Format cli SELF as a useful string.")) 220 (defgeneric handle-unknown-argument (self arg) 221 (:documentation "Handle an unknown argument.")) 223 (defgeneric handle-missing-argument (self arg) 224 (:documentation "Handle a missing argument.")) 226 (defgeneric handle-invalid-argument (self arg) 227 (:documentation "Handle an invalid argument.")) 229 (defgeneric cli-equal (a b)) 231 (defun default-thunk (args opts) 232 (declare (ignore args opts))) 234 (declaim ((vector symbol) *cli-opt-kinds*)) 235 (defvar *cli-opt-kinds* 236 (let ((kinds '(bool str form list sym key num file dir))) 237 (make-array (length kinds) :element-type 'symbol :initial-contents kinds))) 239 (defun cli-opt-kind-p (s) 240 (declare (type symbol s)) 241 (find s *cli-opt-kinds*)) 243 ;; TODO 2024-03-16: this should map directly to Lisp types (fixnum, boolean, etc) 245 (defmacro make-opt-parser (kind-spec &body body) 246 "Return a KIND-opt-parser function based on KIND-SPEC which is either a 247 symbol from *cli-opt-kinds* or a list, and optional BODY which 248 is a list of handlers for the opt-val." 249 (let* ((kind (if (consp kind-spec) (car kind-spec) kind-spec)) 250 (super (when (consp kind-spec) (cadr kind-spec))) 251 (fn-name (symbolicate 'parse- kind '-opt))) 253 (let ((fn1 (when (not (eql 'nil super)) (symbolicate 'parse- super '-opt)))) 255 (defun ,fn-name ($val) 256 "Parse the cli-opt-val $VAL." 258 (when (not (eql ',fn1 'nil)) (setq $val (funcall ',fn1 $val))) 261 (make-opt-parser bool $val) 263 (make-opt-parser str (when (stringp $val) $val)) 265 (make-opt-parser (form str) (read-from-string $val)) 267 (make-opt-parser (list form) (when (listp $val) $val)) 269 (make-opt-parser (sym form) (when (symbolp $val) $val)) 271 (make-opt-parser (key form) (when (keywordp $val) $val)) 273 (make-opt-parser (num form) (when (numberp $val) $val)) 275 (make-opt-parser (file str) 276 (when $val (pathname (the simple-string (parse-native-namestring $val nil *default-pathname-defaults* :as-directory nil))))) 278 (make-opt-parser (dir str) 279 (when $val (sb-ext:parse-native-namestring $val nil *default-pathname-defaults* :as-directory t)))) 283 ;; note that cli-opts can have a nil or unbound name slot 284 (name "" :type string) 285 (kind 'bool :type symbol) 286 (thunk nil :type (or null function symbol)) 288 (global nil :type boolean) 289 (description nil :type (or null string)) 290 (lock nil :type boolean)) 292 (defmethod handle-unknown-argument ((self cli-opt) arg)) 293 (defmethod handle-missing-argument ((self cli-opt) arg)) 294 (defmethod handle-invalid-argument ((self cli-opt) arg)) 296 (defmethod initialize-instance :after ((self cli-opt) &key) 297 (with-slots (name thunk) self 298 (unless (stringp name) (setf name (format nil "~(~A~)" name))) 299 (when (symbolp thunk) (setf thunk (funcall (compile nil `(lambda () ,(symbol-function thunk)))))) 302 (defmethod install-thunk ((self cli-opt) (lambda function) &optional compile) 303 "Install THUNK into the corresponding slot in cli-cmd SELF." 304 (let ((%thunk (if compile (compile nil lambda) lambda))) 305 (setf (cli-thunk self) %thunk) 308 (defmethod print-object ((self cli-opt) stream) 309 (print-unreadable-object (self stream :type t) 310 (format stream "~A :global ~A :val ~A" 312 (cli-opt-global self) 313 (cli-opt-val self)))) 315 (defmethod print-usage ((self cli-opt) &optional stream) 316 (format stream "-~(~{~A~^/--~}~)~A~A" 317 (let ((n (cli-opt-name self))) 318 (declare (simple-string n)) 319 (list (make-shorty n) n)) 320 (if (cli-opt-global self) "* " " ") 321 (if-let ((d (and (slot-boundp self 'description) (cli-opt-description self)))) 322 (format stream ": ~A" (the simple-string d)) 325 (defmethod cli-equal ((a cli-opt) (b cli-opt)) 326 (with-slots (name global kind) a 327 (with-slots ((bn name) (bg global) (bk kind)) b 332 (defmethod call-opt ((self cli-opt) arg) 333 (when-let ((thunk (cli-opt-thunk self))) 334 (setf (cli-opt-val self) (funcall thunk arg)))) 336 (defmethod do-opt ((self cli-opt)) 337 (call-opt self (cli-opt-val self))) 340 ;; name slot is required and must be a string 341 ((name :initarg :name :initform (required-argument :name) :accessor cli-name :type string) 342 (opts :initarg :opts :initform (make-array 0 :element-type 'cli-opt :adjustable t) 343 :accessor cli-opts :type (vector cli-opt)) 344 (cmds :initarg :cmds :initform (make-array 0 :element-type 'cli-cmd :adjustable t) 345 :accessor cli-cmds :type (vector cli-cmd)) 346 (thunk :initform #'default-thunk :initarg :thunk :accessor cli-thunk :type function-lambda-expression) 347 (lock :initform nil :initarg :lock :accessor cli-lock-p :type boolean) 348 (description :initarg :description :accessor cli-description :type string) 349 (args :initform nil :initarg :args :accessor cli-cmd-args)) 350 (:documentation "CLI command class inherited by both the 'main' command which is executed when 351 a CLI is called without arguments, and all subcommands.")) 353 (defmethod initialize-instance :after ((self cli-cmd) &key) 354 (with-slots (name cmds opts thunk) self 355 (unless (stringp name) (setf name (format nil "~(~A~)" name))) 356 (unless (vectorp cmds) (setf cmds (funcall (compile nil `(lambda () ,cmds))))) 357 (unless (vectorp opts) (setf opts (funcall (compile nil `(lambda () ,opts))))) 358 (when (symbolp thunk) (setf thunk (symbol-function thunk))) 361 (defmethod print-object ((self cli-cmd) stream) 362 (print-unreadable-object (self stream :type t) 363 (format stream "~A :opts ~A :cmds ~A :args ~A" 365 (length (cli-opts self)) 366 (length (cli-cmds self)) 367 (length (cli-cmd-args self))))) 369 (defmethod print-usage ((self cli-cmd) &optional stream) 370 (with-slots (opts cmds) self 371 (format stream "~(~A~) ~A~A~A" 373 (if-let ((d (and (slot-boundp self 'description) (cli-description self)))) 374 (format nil ": ~A" d) 378 (format nil "~{~% ~A~^~}" (loop for o across opts collect (print-usage o nil)))) 381 (format nil "~{!~A~}" (loop for c across cmds collect (print-usage c nil))))))) 383 (defmethod push-cmd ((self cli-cmd) (place cli-cmd)) 384 (vector-push self (cli-cmds place))) 386 (defmethod push-opt ((self cli-opt) (place cli-cmd)) 387 (vector-push self (cli-opts place))) 389 (defmethod pop-cmd ((self cli-cmd)) 390 (vector-pop (cli-cmds self))) 392 (defmethod pop-opt ((self cli-opt)) 393 (vector-pop (cli-opts self))) 395 (defmethod cli-equal ((a cli-cmd) (b cli-cmd)) 396 (with-slots (name opts cmds) a 397 (with-slots ((bn name) (bo opts) (bc cmds)) b 398 (and (string= name bn) 399 (if (and (null opts) (null bo)) 401 (unless (member nil (loop for oa across opts 403 collect (cli-equal oa ob))) 405 (if (and (null cmds) (null bc)) 407 (unless (member nil (loop for ca across cmds 409 collect (cli-equal ca cb))) 412 ;; typically when starting from a top-level CLI, the global 413 ;; CLI-OPTS will be parsed first, followed by the first command 414 ;; found. If a command is found, the tail of the list is passed as 415 ;; arguments to this function, which can pass additonal arguments to 418 ;; TODO 2023-09-12: Parsing restarts at the `*cli-group-separator*' 419 ;; if present, or stops at EOI. 421 (defstruct (cli-node (:constructor make-cli-node (kind form))) kind form) 423 (defstruct (cli-ast (:constructor make-cli-ast (ast))) ast) 425 (defmethod find-cmd ((self cli-cmd) name &optional active) 426 (when-let ((c (find name (cli-cmds self) :key #'cli-name :test #'string=))) 428 ;; maybe issue warning here? report to user 434 (defmethod active-cmds ((self cli-cmd)) 435 (remove-if-not #'cli-lock-p (cli-cmds self))) 437 (defmethod find-opts ((self cli-cmd) name &key active recurse) 439 (flet ((%find (o obj) 440 (when-let ((found (find o (cli-opts obj) :key #'cli-opt-name :test 'equal))) 442 (when (and recurse (cli-cmds self)) 443 (loop for c across (cli-cmds self) 447 (setf ret (remove-if-not #'cli-lock-p ret))) 450 (defun active-global-opt-p (opt) 451 "Return non-nil if OPT is active at runtime and global." 452 (and (cli-opt-lock opt) (cli-opt-global opt))) 454 (defmethod active-opts ((self cli-cmd) &optional global) 457 #'active-global-opt-p 461 (defmethod find-short-opts ((self cli-cmd) ch &key recurse) 463 (flet ((%find (ch obj) 464 (when-let ((found (find ch (cli-opts obj) :key #'cli-opt-name :test #'opt-string-prefix-eq))) 466 (when (and recurse (cli-cmds self)) 467 (loop for c across (cli-cmds self) 472 (defun %compose-short-opt (o arg) 473 (declare (ignorable arg)) 474 (setf (cli-opt-val o) t) 475 (make-cli-node 'opt o)) 477 (defun %compose-long-opt (o args) 478 (declare (ignorable args)) 479 (setf (cli-opt-val o) (or (pop args) t)) 480 (make-cli-node 'opt o)) 482 (defmethod proc-args ((self cli-cmd) args) 483 "Process ARGS into an ast. Each element of the ast is a node with a 484 :kind slot, indicating the type of node and a :form slot which stores 487 For now we parse group separators '--' and insert a nil into the tree, 488 this will likely change to generating a new branch in the ast as it 491 (let ((holes)) ;; list of arg indexes which can be skipped since they're 492 ;; consumed by an opt 494 for i below (length args) 495 for (a . args) on args 497 do (continue) ;; skip args which have been consumed already 498 else if (= (length a) 1) 499 collect (make-cli-node 'arg a) ; always treat single-char as arg 500 else if (short-opt-p a) ;; SHORT OPT 501 collect (if-let ((o (find-short-opts self (aref a 1) :recurse t))) 502 (%compose-short-opt (car o) a) 503 (make-cli-node 'arg a)) 504 else if (long-opt-p a) ;; LONG OPT 505 collect (if-let ((o (find-opts self (string-left-trim "-" a) :recurse t))) 506 (prog1 (%compose-long-opt (car o) args) 508 (make-cli-node 'arg a)) 510 else if (opt-group-p a) 514 collect (let ((cmd (find-cmd self a))) 517 (make-cli-node 'cmd (find-cmd self a)) 519 (make-cli-node 'arg a))))))) 521 (declaim (inline solop)) 523 (and (= 0 (length (active-cmds self)) (length (active-opts self))))) 525 (defmethod install-ast ((self cli-cmd) (ast cli-ast)) 526 "Install the given AST, recursively filling in value slots." 527 (with-slots (cmds opts) self 528 ;; we assume all nodes in the ast have been validated and the ast 529 ;; itself is consumed. validation is performed in proc-args. 531 ;; before doing anything else we lock SELF, which should remain 532 ;; locked for the full runtime duration or until GC. 533 (setf (cli-lock-p self) t) 535 for (node . tail) on (debug! (cli-ast-ast ast)) 538 (with-slots (kind form) node 542 (let ((name (cli-opt-name form))) 543 (when-let ((o (car (find-opts self name)))) 545 (setf (cli-opt-lock o) t)))) 546 ;; when we encounter a command we recurse over the tail 548 (when-let ((c (find-cmd self (cli-name form)))) 549 ;; handle the rest of the AST 550 (setf c (install-ast c (make-cli-ast tail))) 551 (return-from install))) 552 (arg (push-arg form self))))) 553 (setf (cli-cmd-args self) (nreverse (cli-cmd-args self))) 556 (defmethod install-thunk ((self cli-cmd) (lambda function) &optional compile) 557 "Install THUNK into the corresponding slot in cli-cmd SELF." 558 (let ((%thunk (if compile (compile nil lambda) lambda))) 559 (setf (cli-thunk self) %thunk) 562 (defmethod push-arg (arg (self cli-cmd)) 563 (push arg (cli-cmd-args self))) 565 (defmethod parse-args ((self cli-cmd) args &key (compile t)) 566 "Parse ARGS and return the updated object SELF. 568 ARGS is assumed to be a valid cli-ast (list of cli-nodes), unless 569 COMPILE is t, in which case a list of strings is assumed." 570 (with-slots (opts cmds) self 571 (let ((args (if compile (proc-args self args) args))) 572 (install-ast self args)))) 574 ;; warning: make sure to fill in the opt and cmd slots with values 575 ;; from the top-level args before calling a command. 576 (defmethod call-cmd ((self cli-cmd) args opts) 578 (funcall (cli-thunk self) args opts)) 580 (defmethod do-cmd ((self cli-cmd)) 582 (call-cmd self (cli-cmd-args self) (active-opts self)) 584 (loop for o across (active-opts self) 586 (loop for c across (active-cmds self) 589 (defclass cli (cli-cmd) 590 ;; name slot defaults to *package*, must be string 591 ((name :initarg :name :initform (string-downcase (package-name *package*)) :accessor cli-name :type string) 592 (version :initarg :version :initform "0.1.0" :accessor cli-version :type string) 593 ;; TODO 2023-10-11: look into pushd popd - cd-stack? 594 (cd :initarg :cd :initform (sb-posix:getcwd) :type string :accessor cli-cd 595 :documentation "working directory of the top-level CLI.")) 596 (:documentation "CLI")) 598 (defmethod print-usage ((self cli) &optional stream) 599 (iprintln (format nil "usage: ~A [global] <command> [<arg>]~%" (cli-name self)) 2 stream)) 601 (defmethod print-version ((self cli) &optional stream) 602 (println (cli-version self) stream)) 604 (defmethod print-help ((self cli) &optional stream) 605 (println (format nil "~A v~A --- ~A~%" (cli-name self) (cli-version self) (cli-description self)) stream) 606 (print-usage self stream) 608 (println "options:" stream) 609 (with-slots (opts cmds) self 611 (loop for o across opts 612 do (iprintln (print-usage o) 2 stream))) 614 (println "commands:" stream) 616 (loop for c across cmds 617 do (iprintln (print-usage c) 2 stream))))) 619 (defmethod cli-equal :before ((a cli) (b cli)) 620 "Return T if A is the same cli object as B. 622 Currently this function is intended only for instances of the CLI 623 class and is used as a specialized EQL for DEFINE-CONSTANT." 624 (with-slots (version) a 625 (with-slots ((bv version)) b 626 (string= version bv)))) 628 (declaim (inline debug-opts)) 629 (defun debug-opts (cli) 630 (let ((o (active-opts cli)) 631 (a (cli-cmd-args cli)) 632 (c (active-cmds cli))) 633 (log:debug! (cli-cd cli) o a c))) 637 ;; TODO this is intended to be a simplified functional argument parser 638 ;; which is completely compatible with the toplevel SBCL options. 640 ;; Instead of consuming the args into an AST, we loop over command 641 ;; line options in a lexical context, binding individual symbols. 643 (defun namestring-to-opt (str) (sb-int:symbolicate (string-upcase (trim str :char-bag '(#\-))))) 645 (defvar *default-opt-handlers* 647 (lambda (o) (cons (namestring-to-opt o) #'set)) 648 sb-impl::+runtime-options+)) 650 ;; TODO 2024-03-19: need a way to terminate the loop early. (throw/catch) 652 ;; do handlers need to be able to set multiple symbols? 654 ;; should we define opts as special symbols in their own package? (defpackage :OPTS) 655 (defvar *opt-handlers* *default-opt-handlers*) 657 (defun find-opt-handler (str) 658 (find (namestring-to-opt str) *opt-handlers* :key #'car)) 660 (defmacro with-opts-handled (&body body) 661 (let* ((syms (mapcar #'car *opt-handlers*))) 662 `(let ((opts (cdr *posix-argv*)) 663 ,@(mapcar #'list syms)) 664 (declare (type list opts)) 668 (sb-impl::startup-error "unexpected end of cli opts")))) 670 (if-let ((opt (find-opt-handler (car opts)))) 671 (apply (cdr opt) (car opt) ($pop)))) 673 (setf (cdr *posix-argv*) opts)) 678 ;; These macros help with defining a toplevel initialization 679 ;; function. Initialization functions are responsible for parsing runtime 680 ;; options and starting a REPL if needed. 681 ;; (defmacro define-toplevel-init (name (props opts) &body body)) 682 ;; (defmacro define-toplevel-repl (name (props opts) &body body)) 684 (defun default-toplevel-init () 685 (let ((opts (cdr *posix-argv*)) 687 (declare (type list opts)) 691 (sb-impl::startup-error "unexpected end of cli opts")))) 693 (let ((opt (car opts))) 695 ((string= opt "--sysinit") 698 (sb-impl::startup-error "multiple --sysinit opts") 699 (setf sysinit ($pop)))) 701 (if (find "--end-toplevel-options" opts 703 (sb-impl::startup-error "bad toplevel opt: ~S" 707 (setf (cdr *posix-argv*) opts)))))