1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2025-12-15 10:30:25 -08:00

Convert Eshell globs ahead of time instead of doing it repeatedly

* lisp/eshell/em-glob.el (eshell-glob-recursive): New variable.
(eshell-glob-convert-1, eshell-glob-convert): New functions.
(eshell-extended-glob): Use 'eshell-glob-convert'.
(eshell-glob-entries): Adapt function to use pre-converted globs.

* test/lisp/eshell-em-glob-tests.el (em-glob-test/match-dot-files):
New test.
This commit is contained in:
Jim Porter 2022-06-24 08:39:42 -07:00 committed by Lars Ingebrigtsen
parent 598d7c5d1c
commit ea3681575f
2 changed files with 129 additions and 90 deletions

View file

@ -183,6 +183,10 @@ interpretation."
(defvar eshell-glob-matches)
(defvar message-shown)
(defvar eshell-glob-recursive-alist
'(("**/" . recurse)
("***/" . recurse-symlink)))
(defun eshell-glob-regexp (pattern)
"Convert glob-pattern PATTERN to a regular expression.
The basic syntax is:
@ -232,6 +236,74 @@ resulting regular expression."
(regexp-quote (substring pattern matched-in-pattern))
"\\'")))
(defun eshell-glob-convert-1 (glob &optional last)
"Convert a GLOB matching a single element of a file name to regexps.
If LAST is non-nil, this glob is the last element of a file name.
The result is a pair of regexps, the first for file names to
include, and the second for ones to exclude."
(let ((len (length glob)) (index 1) (incl glob) excl)
;; We can't use `directory-file-name' because it strips away text
;; properties in the string.
(let ((last (1- (length incl))))
(when (eq (aref incl last) ?/)
(setq incl (substring incl 0 last))))
;; Split the glob if it contains a negation like x~y.
(while (and (eq incl glob)
(setq index (string-search "~" glob index)))
(if (or (get-text-property index 'escaped glob)
(or (= (1+ index) len)))
(setq index (1+ index))
(setq incl (substring glob 0 index)
excl (substring glob (1+ index)))))
(setq incl (eshell-glob-regexp incl)
excl (and excl (eshell-glob-regexp excl)))
;; Exclude dot files if requested.
(if (or eshell-glob-include-dot-files
(eq (aref glob 0) ?.))
(unless (or eshell-glob-include-dot-dot
(not last))
(setq excl (if excl
(concat "\\(\\`\\.\\.?\\'\\|" excl "\\)")
"\\`\\.\\.?\\'")))
(setq excl (if excl
(concat "\\(\\`\\.\\|" excl "\\)")
"\\`\\.")))
(cons incl excl)))
(defun eshell-glob-convert (glob)
"Convert an Eshell glob-pattern GLOB to regexps.
The result is a list, where the first element is the base
directory to search in, and the second is a list containing
elements of the following forms:
* Regexp pairs as generated by `eshell-glob-convert-1'.
* `recurse', indicating that searches should recurse into
subdirectories.
* `recurse-symlink', like `recurse', but also following symlinks."
(let ((globs (eshell-split-path glob))
start-dir result last-saw-recursion)
(if (and (cdr globs)
(file-name-absolute-p (car globs)))
(setq start-dir (car globs)
globs (cdr globs))
(setq start-dir "."))
(while globs
(if-let ((recurse (cdr (assoc (car globs)
eshell-glob-recursive-alist))))
(if last-saw-recursion
(setcar result recurse)
(push recurse result)
(setq last-saw-recursion t))
(push (eshell-glob-convert-1 (car globs) (null (cdr globs)))
result)
(setq last-saw-recursion nil))
(setq globs (cdr globs)))
(list (file-name-as-directory start-dir)
(nreverse result))))
(defun eshell-extended-glob (glob)
"Return a list of files matched by GLOB.
If no files match, signal an error (if `eshell-error-if-no-glob'
@ -247,14 +319,10 @@ syntax. Things that are not supported are:
Mainly they are not supported because file matching is done with Emacs
regular expressions, and these cannot support the above constructs."
(let ((paths (eshell-split-path glob))
(let ((globs (eshell-glob-convert glob))
eshell-glob-matches message-shown)
(unwind-protect
(if (and (cdr paths)
(file-name-absolute-p (car paths)))
(eshell-glob-entries (file-name-as-directory (car paths))
(cdr paths))
(eshell-glob-entries (file-name-as-directory ".") paths))
(apply #'eshell-glob-entries globs)
(if message-shown
(message nil)))
(or (and eshell-glob-matches (sort eshell-glob-matches #'string<))
@ -263,94 +331,50 @@ regular expressions, and these cannot support the above constructs."
glob))))
;; FIXME does this really need to abuse eshell-glob-matches, message-shown?
(defun eshell-glob-entries (path globs &optional recurse-p)
"Glob the entries in PATH, possibly recursing if RECURSE-P is non-nil."
(defun eshell-glob-entries (path globs)
"Match the entries in PATH against GLOBS.
GLOBS is a list of globs as converted by `eshell-glob-convert',
which see."
(let* ((entries (ignore-errors
(file-name-all-completions "" path)))
(case-fold-search eshell-glob-case-insensitive)
(glob (car globs))
(len (length glob))
dirs rdirs
incl excl
name isdir pathname)
(while (cond
((and (= len 3) (equal glob "**/"))
(setq recurse-p 2
globs (cdr globs)
glob (car globs)
len (length glob)))
((and (= len 4) (equal glob "***/"))
(setq recurse-p 3
globs (cdr globs)
glob (car globs)
len (length glob)))))
(if (and recurse-p (not glob))
(error "`**/' cannot end a globbing pattern"))
(let ((index 1))
(setq incl glob)
(while (and (eq incl glob)
(setq index (string-search "~" glob index)))
(if (or (get-text-property index 'escaped glob)
(or (= (1+ index) len)))
(setq index (1+ index))
(setq incl (substring glob 0 index)
excl (substring glob (1+ index))))))
;; can't use `directory-file-name' because it strips away text
;; properties in the string
(let ((len (1- (length incl))))
(if (eq (aref incl len) ?/)
(setq incl (substring incl 0 len)))
(when excl
(setq len (1- (length excl)))
(if (eq (aref excl len) ?/)
(setq excl (substring excl 0 len)))))
(setq incl (eshell-glob-regexp incl)
excl (and excl (eshell-glob-regexp excl)))
(if (or eshell-glob-include-dot-files
(eq (aref glob 0) ?.))
(unless (or eshell-glob-include-dot-dot
(cdr globs))
(setq excl (if excl
(concat "\\(\\`\\.\\.?\\'\\|" excl "\\)")
"\\`\\.\\.?\\'")))
(setq excl (if excl
(concat "\\(\\`\\.\\|" excl "\\)")
"\\`\\.")))
(file-name-all-completions "" path)))
(case-fold-search eshell-glob-case-insensitive)
glob glob-remainder recurse-p)
(if (rassq (car globs) eshell-glob-recursive-alist)
(setq recurse-p (car globs)
glob (cadr globs)
glob-remainder (cddr globs))
(setq glob (car globs)
glob-remainder (cdr globs)))
(when (and recurse-p eshell-glob-show-progress)
(message "Building file list...%d so far: %s"
(length eshell-glob-matches) path)
(length eshell-glob-matches) path)
(setq message-shown t))
(if (equal path "./") (setq path ""))
(while entries
(setq name (car entries)
len (length name)
isdir (eq (aref name (1- len)) ?/))
(if (let ((fname (directory-file-name name)))
(and (not (and excl (string-match excl fname)))
(string-match incl fname)))
(if (cdr globs)
(if isdir
(setq dirs (cons (concat path name) dirs)))
(setq eshell-glob-matches
(cons (concat path name) eshell-glob-matches))))
(if (and recurse-p isdir
(or (> len 3)
(not (or (and (= len 2) (equal name "./"))
(and (= len 3) (equal name "../")))))
(setq pathname (concat path name))
(not (and (= recurse-p 2)
(file-symlink-p
(directory-file-name pathname)))))
(setq rdirs (cons pathname rdirs)))
(setq entries (cdr entries)))
(setq dirs (nreverse dirs)
rdirs (nreverse rdirs))
(while dirs
(eshell-glob-entries (car dirs) (cdr globs))
(setq dirs (cdr dirs)))
(while rdirs
(eshell-glob-entries (car rdirs) globs recurse-p)
(setq rdirs (cdr rdirs)))))
(when (equal path "./") (setq path ""))
(let ((incl (car glob))
(excl (cdr glob))
dirs rdirs)
(dolist (name entries)
(let* ((len (length name))
(isdir (eq (aref name (1- len)) ?/))
pathname)
(when (let ((fname (directory-file-name name)))
(and (not (and excl (string-match excl fname)))
(string-match incl fname)))
(if glob-remainder
(when isdir
(push (concat path name) dirs))
(push (concat path name) eshell-glob-matches)))
(when (and recurse-p isdir
(not (member name '("./" "../")))
(setq pathname (concat path name))
(not (and (eq recurse-p 'recurse)
(file-symlink-p
(directory-file-name pathname)))))
(push pathname rdirs))))
(dolist (dir (nreverse dirs))
(eshell-glob-entries dir glob-remainder))
(dolist (rdir (nreverse rdirs))
(eshell-glob-entries rdir globs)))))
(provide 'em-glob)

View file

@ -160,6 +160,21 @@ component ending in \"symlink\" is treated as a symbolic link."
(should (equal (eshell-extended-glob "[[:digit:]]##~4?")
'("1" "12" "123")))))
(ert-deftest em-glob-test/match-dot-files ()
"Test that dot files are matched correctly."
(with-fake-files '("foo.el" ".emacs")
(should (equal (eshell-extended-glob ".*")
'("../" "./" ".emacs")))
(let (eshell-glob-include-dot-dot)
(should (equal (eshell-extended-glob ".*")
'(".emacs"))))
(let ((eshell-glob-include-dot-files t))
(should (equal (eshell-extended-glob "*")
'("../" "./" ".emacs" "foo.el")))
(let (eshell-glob-include-dot-dot)
(should (equal (eshell-extended-glob "*")
'(".emacs" "foo.el")))))))
(ert-deftest em-glob-test/no-matches ()
"Test behavior when a glob fails to match any files."
(with-fake-files '("foo.el" "bar.el")