Re: Parsing unloaded .el files for defun interactive forms

[email protected]
Newsgroups gmane.emacs.help
Message-ID <[email protected]>
Heime <[email protected]> writes:

> I've struggled to list interactive functions from .el files using obarray 
> symbols, as it requires prior loading.  I need to parse unloaded .el files 
> directly to extract defun forms with (interactive ...).
>
> Current broken code:
>
> (defun ramazones-smbptn (smb pattern file)
>   "Filter symbol SMB defined in a specific file that are currently loaded."
>   (when (and (commandp smb)
>              (string-match-p pattern (symbol-name smb)))
>     smb))
>
> (defun ramazonas-agfun (file pattern)
>   "List all interactive functions defined in FILE."
>   
>   (interactive
>     (list 
>       (read-file-name "File: ")
>       (read-regexp "Symbol Pattern (RET for all): ")))
>
>   (let ( (smbs   nil)
>          (count  0) )
>     (when (find-file-noselect file)
>       (mapatoms (lambda (smb)
>                   (when (ramazonas-smbptn smb pattern file)
>                     (setq count (1+ count))
>                     (push smb smbs))))
>
>       (if (> count 0)
>           (progn (message "Interactive functions: %s"
>                    (seq-filter #'commandp smbs))
>                  (seq-filter #'commandp smbs))
>         (message "No interactive functions found")))))
>

1. You should be able to look up the symbols of files that
   Emacs has loaded, as the ‘let’ expressions demonstrated.
   But, as you point out, it does not work for Emacs Lisp
   files whose code has not yet been loaded.

2. The code does not check the values of ‘file’, whether it
   is provided interactively or non-interactively.  It
   should be changed to do that.

      (when (find-file-noselect file) ...)

    returns nil when ‘file’ has a value that is does not
    correctly specify an existing, readable file.  Before
    this expression is evaluated, the value assigned to ‘file’
    should be checked.  For example,

       (if (file-exists-p file)
         ;; then code
         ;; else
         (error "No such file %s" file))

    or

       (if (file-readable-p file)
         ;; then code
         ;; else
         (error "Cannot read file %s" file))

    See:
       (info "(elisp) Testing Accessibility")

3. The function ‘find-file-noselect’ reads the text of a
   file into an Emacs buffer, but this is not what your
   function needs.  Instead, it needs to "load" the Emacs
   Lisp code in the file, that is, it reads the Emacs Lisp
   expressions in the file and evaluates them.

     (info "(emacs) Lisp Libraries")
     (info "(elisp) How Programs Do Loading")

4. When ‘file’ is specified using "~/...", then the
   expression (string= (symbol-file smb) file) will
   return nil because (symbol-file smb) returns the full
   pathname to the file.  In order to prevent this, ‘file’
   should be expanded to the full pathname using ‘expand-file-name’

   C-h f symbol-file
    "The value is normally an absolute file name.  It can also be nil,
     if the definition is not associated with any file.  If SYMBOL
     specifies an autoloaded function, the value can be a relative
     file name without extension."

    So, for autoloaded functions, the code can still fail.

5. The expression (seq-filter #'commandp smbs) is not needed
   because the symbols have already been filtered to create a
   list of commands, that is, (seq-filter #'commandp smbs) =
   smbs.

6. Likewise, ‘count’ is not needed (it was added to help
   with debugging).  (if (> count 0) ...) can be replaced
   with (if smbs ...)

7. The helper function ‘ramazonas-smbptn’ is called many
   times, passing ‘pattern’ and ‘file’, which do not change.
   Replace

    (when (ramazonas-smbptn smb pattern file)
      (setq count (1+ count))
      (push smb smbs))

   with

     (when (and (commandp smb)
                (string-match-p pattern (symbol-name smb))
                (string= (symbol-file smb) full-name))
       (push smb smbs))

   and delete the definition of ‘ramazonas-smbptn’.

Edited version with the suggested changes, above:

(defun mazonas-agfun (file pattern)
  "List all commands defined in FILE whose names match PATTERN."
  (interactive
   (list
    (read-file-name "File: ")
    (read-regexp "Symbol Pattern (RET for all): ")))
  
  (if (file-readable-p file)
      (let ((smbs nil)
            (full-name (expand-file-name file)))
        (load-file full-name)
        (mapatoms (lambda (smb)
                    (when (and (commandp smb)
                               (string-match-p pattern (symbol-name smb))
                               (string= (symbol-file smb) full-name))
                      (push smb smbs))))
        (when smbs
          (message "%S" smbs))
        smbs)
    (error "Cannot read file %s" file)))

With these changes, ‘mazonas-agfun’ has three possible
results:
   1. Return a list of loaded symbols for ‘file’ that match ‘pattern’
   2. nil, when there are no matches
   3. Error, if the file does not exist or is unreadable.

It can also have a run-time error if there are errors in the
Emacs Lisp expressions in ‘file’ that cause ‘load-file’ to fail.

-- 
The lyf so short, the craft so long to lerne.
- Geoffrey Chaucer, The Parliament of Birds.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.