;;; ---------------------------------------------------------------------------
;;; CommandScout.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; Lists every custom command defined by a LISP file WITHOUT loading it.
;;;
;;; This becomes indispensable the moment a library passes about fifty
;;; routines. You are handed a .lsp file, or you find one in a folder, and you
;;; need to know what commands it would add before you commit to loading it -
;;; because loading it might redefine a command you already rely on, or run
;;; startup code you have not read.
;;;
;;; HOW IT WORKS WITHOUT LOADING
;;; The file is read as plain text and scanned with a regular expression for
;;; the pattern that defines a command:
;;;
;;;     \(\s*defun\s+c:(\S+)
;;;
;;; which reads as: an opening bracket, optional whitespace, the word "defun",
;;; whitespace, "c:", and then captures the run of non-space characters that
;;; follows - the command name. Because this is pure text matching, no code
;;; from the file is ever evaluated, so a file cannot do anything to your
;;; session merely by being inspected.
;;;
;;; The regular expression engine and the file reader both come from Windows
;;; Scripting COM objects rather than from AutoLISP, which has no native regex.
;;;
;;;   CMDSCOUT  - list the commands defined by a chosen LISP file
;;; ---------------------------------------------------------------------------

(vl-load-com)

;; ---------------------------------------------------------------------------
;; CommandScout:Extract
;; ---------------------------------------------------------------------------
;; Returns a list of every command name defined in the given LISP file, or nil
;; if the file could not be read.
;;
;; TWO COM OBJECTS ARE CREATED HERE
;;   scripting.filesystemobject - reads the file's entire text in one go
;;   vbscript.regexp            - performs the pattern match
;;
;; Both are external objects, and both MUST be released. A COM object that is
;; not released stays alive in memory for the rest of the AutoCAD session; do
;; that once per file across a large folder and the leak becomes real. The
;; release loop at the end therefore runs unconditionally, whether the scan
;; succeeded, failed or threw.
;;
;; The scan itself is wrapped in vl-catch-all-apply for the same reason - if it
;; throws part way through, execution still reaches the release loop rather
;; than abandoning the objects in memory.
;;
;; lsp - [str] path to the AutoLISP file to inspect
;; ---------------------------------------------------------------------------
(defun CommandScout:Extract ( lsp / err fso mch rgx rtn stm obj )

    (if (and (setq lsp (findfile lsp))
             (setq fso (vlax-create-object "scripting.filesystemobject"))
             (setq rgx (vlax-create-object "vbscript.regexp"))
        )
        (setq err
            (vl-catch-all-apply
               '(lambda ( / str )

                    ;; opentextfile mode 1 is read-only; the trailing 0 means
                    ;; ASCII rather than Unicode. readall pulls the whole file
                    ;; into a single string, which is what the regex needs.
                    (setq stm (vlax-invoke-method fso 'opentextfile lsp 1 nil 0)
                          str (vlax-invoke-method stm 'readall)
                    )
                    (vlax-invoke-method stm 'close)

                    ;; global    - find every match, not just the first
                    ;; ignorecase- DEFUN and defun are both valid
                    ;; multiline - ^ and $ match at each line, not just the ends
                    (vlax-put-property rgx 'global     actrue)
                    (vlax-put-property rgx 'ignorecase actrue)
                    (vlax-put-property rgx 'multiline  actrue)
                    (vlax-put-property rgx 'pattern    "\\(\\s*defun\\s+c:(\\S+)")

                    ;; Each match carries a submatches collection holding the
                    ;; bracketed capture group - the command name itself.
                    (if (setq mch (vlax-invoke rgx 'execute str))
                        (vlax-for itm mch
                            (vlax-for sub (vlax-get itm 'submatches)
                                (setq rtn (cons sub rtn))
                            )
                        )
                    )
                )
            )
        )
    )

    ;; Release everything that was successfully created, in every circumstance.
    (foreach obj (list mch stm fso rgx)
        (if (and (= 'vla-object (type obj))
                 (not (vlax-object-released-p obj))
            )
            (vlax-release-object obj)
        )
    )

    (if (vl-catch-all-error-p err)
        (progn (princ (strcat "\nCould not scan file: " (vl-catch-all-error-message err)))
               nil
        )
        (reverse rtn)
    )
)

;; ---------------------------------------------------------------------------
;; c:CMDSCOUT  -  main routine
;; ---------------------------------------------------------------------------
(defun c:CMDSCOUT ( / *error* lsp lst cmd )

    ;; Read-only inspection: nothing in the drawing is touched, no system
    ;; variables are changed, and no undo group is required.
    (defun *error* ( msg )
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** CMDSCOUT error: " msg " **"))
        )
        (princ)
    )

    (cond
        ;; getfiled flag 16 means the file must already exist - you cannot
        ;; scout a file that is not there.
        (   (not (setq lsp (getfiled "Select AutoLISP File to Scout" "" "lsp" 16)))
            (princ "\n*Cancelled*")
        )

        (   (not (setq lst (CommandScout:Extract lsp)))
            (princ (strcat "\nNo custom commands found in " (vl-filename-base lsp) ".lsp"))
        )

        (   t
            (princ (strcat "\nCommands defined by " (vl-filename-base lsp) ".lsp:"))
            (princ "\n------------------------------------------------------------")

            ;; Upper-cased for consistency, then sorted alphabetically so a
            ;; long list can actually be scanned by eye.
            (foreach cmd (acad_strlsort (mapcar 'strcase lst))
                (princ (strcat "\n    " cmd))
            )

            (princ "\n------------------------------------------------------------")
            (princ (strcat "\n" (itoa (length lst))
                           " command" (if (= 1 (length lst)) "" "s") " found."
                   )
            )

            ;; Bring the text window forward - on a file defining thirty
            ;; commands the list will have scrolled off the command line.
            (textpage)
        )
    )

    (princ)
)

(princ)
