mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-01-30 04:10:54 -08:00
Add unit tests and documentation for Eshell pattern-based globs
* lisp/eshell/em-glob.el (eshell-extended-glob): Fix docstring. (eshell-glob-entries): Refer to '**/' in error (technically, '**' can end a glob, but it means the same thing as '*'). (Bug#54470) * test/lisp/eshell/em-glob-tests.el: New file. * doc/misc/eshell.texi (Globbing): Document pattern-based globs.
This commit is contained in:
parent
265f4ef702
commit
bbb92dde01
3 changed files with 262 additions and 17 deletions
|
|
@ -1089,15 +1089,91 @@ the result of @var{expr} is not a string or a sequence.
|
|||
|
||||
@node Globbing
|
||||
@section Globbing
|
||||
Eshell's globbing syntax is very similar to that of Zsh. Users coming
|
||||
from Bash can still use Bash-style globbing, as there are no
|
||||
incompatibilities. Most globbing is pattern-based expansion, but there
|
||||
is also predicate-based expansion. @xref{Filename Generation, , ,
|
||||
zsh, The Z Shell Manual},
|
||||
for full syntax. To customize the syntax and behavior of globbing in
|
||||
Eshell see the Customize@footnote{@xref{Easy Customization, , , emacs,
|
||||
The GNU Emacs Manual}.}
|
||||
groups ``eshell-glob'' and ``eshell-pred''.
|
||||
@vindex eshell-glob-case-insensitive
|
||||
Eshell's globbing syntax is very similar to that of Zsh
|
||||
(@pxref{Filename Generation, , , zsh, The Z Shell Manual}). Users
|
||||
coming from Bash can still use Bash-style globbing, as there are no
|
||||
incompatibilities.
|
||||
|
||||
By default, globs are case sensitive, except on MS-DOS/MS-Windows
|
||||
systems. You can control this behavior via the
|
||||
@code{eshell-glob-case-insensitive} option. You can further customize
|
||||
the syntax and behavior of globbing in Eshell via the Customize group
|
||||
``eshell-glob'' (@pxref{Easy Customization, , , emacs, The GNU Emacs
|
||||
Manual}).
|
||||
|
||||
@table @samp
|
||||
|
||||
@item *
|
||||
Matches any string (including the empty string). For example,
|
||||
@samp{*.el} matches any file with the @file{.el} extension.
|
||||
|
||||
@item ?
|
||||
Matches any single character. For example, @samp{?at} matches
|
||||
@file{cat} and @file{bat}, but not @file{goat}.
|
||||
|
||||
@item **/
|
||||
Matches zero or more subdirectories in a file name. For example,
|
||||
@samp{**/foo.el} matches @file{foo.el}, @file{bar/foo.el},
|
||||
@file{bar/baz/foo.el}, etc. Note that this cannot be combined with
|
||||
any other patterns in the same file name segment, so while
|
||||
@samp{foo/**/bar.el} is allowed, @samp{foo**/bar.el} is not.
|
||||
|
||||
@item ***/
|
||||
Like @samp{**/}, but follows symlinks as well.
|
||||
|
||||
@cindex character sets, in Eshell glob patterns
|
||||
@cindex character classes, in Eshell glob patterns
|
||||
@item [ @dots{} ]
|
||||
Defines a @dfn{character set} (@pxref{Regexps, , , emacs, The GNU
|
||||
Emacs Manual}). A character set matches characters between the two
|
||||
brackets; for example, @samp{[ad]} matches @file{a} and @file{d}. You
|
||||
can also include ranges of characters in the set by separating the
|
||||
start and end with @samp{-}. Thus, @samp{[a-z]} matches any
|
||||
lower-case @acronym{ASCII} letter. Note that, unlike in Zsh,
|
||||
character ranges are interpreted in the Unicode codepoint order, not
|
||||
in the locale-dependent collation order.
|
||||
|
||||
Additionally, you can include @dfn{character classes} in a character
|
||||
set. A @samp{[:} and balancing @samp{:]} enclose a character class
|
||||
inside a character set. For instance, @samp{[[:alnum:]]}
|
||||
matches any letter or digit. @xref{Char Classes, , , elisp, The Emacs
|
||||
Lisp Reference Manual}, for a list of character classes.
|
||||
|
||||
@cindex complemented character sets, in Eshell glob patterns
|
||||
@item [^ @dots{} ]
|
||||
Defines a @dfn{complemented character set}. This behaves just like a
|
||||
character set, but matches any character @emph{except} the ones
|
||||
specified.
|
||||
|
||||
@cindex groups, in Eshell glob patterns
|
||||
@item ( @dots{} )
|
||||
Defines a @dfn{group}. A group matches the pattern between @samp{(}
|
||||
and @samp{)}. Note that a group can only match a single file name
|
||||
component, so a @samp{/} inside a group will signal an error.
|
||||
|
||||
@item @var{x}|@var{y}
|
||||
Inside of a group, matches either @var{x} or @var{y}. For example,
|
||||
@samp{e(m|sh)-*} matches any file beginning with @file{em-} or
|
||||
@file{esh-}.
|
||||
|
||||
@item @var{x}#
|
||||
Matches zero or more copies of the glob pattern @var{x}. For example,
|
||||
@samp{fo#.el} matches @file{f.el}, @file{fo.el}, @file{foo.el}, etc.
|
||||
|
||||
@item @var{x}##
|
||||
Matches one or more copies of the glob pattern @var{x}. Thus,
|
||||
@samp{fo#.el} matches @file{fo.el}, @file{foo.el}, @file{fooo.el},
|
||||
etc.
|
||||
|
||||
@item @var{x}~@var{y}
|
||||
Matches anything that matches the pattern @var{x} but not @var{y}. For
|
||||
example, @samp{[[:digit:]]#~4?} matches @file{1} and @file{12}, but
|
||||
not @file{42}. Note that unlike in Zsh, only a single @samp{~}
|
||||
operator can be used in a pattern, and it cannot be inside of a group
|
||||
like @samp{(@var{x}~@var{y})}.
|
||||
|
||||
@end table
|
||||
|
||||
@node Input/Output
|
||||
@chapter Input/Output
|
||||
|
|
|
|||
|
|
@ -233,7 +233,10 @@ resulting regular expression."
|
|||
"\\'")))
|
||||
|
||||
(defun eshell-extended-glob (glob)
|
||||
"Return a list of files generated from GLOB, perhaps looking for DIRS-ONLY.
|
||||
"Return a list of files matched by GLOB.
|
||||
If no files match, signal an error (if `eshell-error-if-no-glob'
|
||||
is non-nil), or otherwise return GLOB itself.
|
||||
|
||||
This function almost fully supports zsh style filename generation
|
||||
syntax. Things that are not supported are:
|
||||
|
||||
|
|
@ -243,12 +246,7 @@ syntax. Things that are not supported are:
|
|||
foo~x(a|b) (a|b) will be interpreted as a predicate/modifier list
|
||||
|
||||
Mainly they are not supported because file matching is done with Emacs
|
||||
regular expressions, and these cannot support the above constructs.
|
||||
|
||||
If this routine fails, it returns nil. Otherwise, it returns a list
|
||||
the form:
|
||||
|
||||
(INCLUDE-REGEXP EXCLUDE-REGEXP (PRED-FUNC-LIST) (MOD-FUNC-LIST))"
|
||||
regular expressions, and these cannot support the above constructs."
|
||||
(let ((paths (eshell-split-path glob))
|
||||
eshell-glob-matches message-shown)
|
||||
(unwind-protect
|
||||
|
|
@ -287,7 +285,7 @@ the form:
|
|||
glob (car globs)
|
||||
len (length glob)))))
|
||||
(if (and recurse-p (not glob))
|
||||
(error "`**' cannot end a globbing pattern"))
|
||||
(error "`**/' cannot end a globbing pattern"))
|
||||
(let ((index 1))
|
||||
(setq incl glob)
|
||||
(while (and (eq incl glob)
|
||||
|
|
|
|||
171
test/lisp/eshell/em-glob-tests.el
Normal file
171
test/lisp/eshell/em-glob-tests.el
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
;;; em-glob-tests.el --- em-glob test suite -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; This file is part of GNU Emacs.
|
||||
|
||||
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Tests for Eshell's glob expansion.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'em-glob)
|
||||
|
||||
(defmacro with-fake-files (files &rest body)
|
||||
"Evaluate BODY forms, pretending that FILES exist on the filesystem.
|
||||
FILES is a list of file names that should be reported as
|
||||
appropriate by `file-name-all-completions'. Any file name
|
||||
component ending in \"symlink\" is treated as a symbolic link."
|
||||
(declare (indent 1))
|
||||
`(cl-letf (((symbol-function 'file-name-all-completions)
|
||||
(lambda (file directory)
|
||||
(cl-assert (string= file ""))
|
||||
(setq directory (expand-file-name directory))
|
||||
`("./" "../"
|
||||
,@(delete-dups
|
||||
(remq nil
|
||||
(mapcar
|
||||
(lambda (file)
|
||||
(setq file (expand-file-name file))
|
||||
(when (string-prefix-p directory file)
|
||||
(replace-regexp-in-string
|
||||
"/.*" "/"
|
||||
(substring file (length directory)))))
|
||||
,files))))))
|
||||
((symbol-function 'file-symlink-p)
|
||||
(lambda (file)
|
||||
(string-suffix-p "symlink" file))))
|
||||
,@body))
|
||||
|
||||
;;; Tests:
|
||||
|
||||
(ert-deftest em-glob-test/match-any-string ()
|
||||
"Test that \"*\" pattern matches any string."
|
||||
(with-fake-files '("a.el" "b.el" "c.txt" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "*.el")
|
||||
'("a.el" "b.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-any-character ()
|
||||
"Test that \"?\" pattern matches any character."
|
||||
(with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "?.el")
|
||||
'("a.el" "b.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-recursive ()
|
||||
"Test that \"**/\" recursively matches directories."
|
||||
(with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
|
||||
"dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
|
||||
(should (equal (eshell-extended-glob "**/a.el")
|
||||
'("a.el" "dir/a.el" "dir/sub/a.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-recursive-follow-symlinks ()
|
||||
"Test that \"***/\" recursively matches directories, following symlinks."
|
||||
(with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
|
||||
"dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
|
||||
(should (equal (eshell-extended-glob "***/a.el")
|
||||
'("a.el" "dir/a.el" "dir/sub/a.el" "dir/symlink/a.el"
|
||||
"symlink/a.el" "symlink/sub/a.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-recursive-mixed ()
|
||||
"Test combination of \"**/\" and \"***/\"."
|
||||
(with-fake-files '("dir/a.el" "dir/sub/a.el" "dir/sub2/a.el"
|
||||
"dir/symlink/a.el" "dir/sub/symlink/a.el" "symlink/a.el"
|
||||
"symlink/sub/a.el" "symlink/sub/symlink/a.el")
|
||||
(should (equal (eshell-extended-glob "**/sub/***/a.el")
|
||||
'("dir/sub/a.el" "dir/sub/symlink/a.el")))
|
||||
(should (equal (eshell-extended-glob "***/sub/**/a.el")
|
||||
'("dir/sub/a.el" "symlink/sub/a.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-character-set-individual ()
|
||||
"Test \"[...]\" for individual characters."
|
||||
(with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "[ab].el")
|
||||
'("a.el" "b.el")))
|
||||
(should (equal (eshell-extended-glob "[^ab].el")
|
||||
'("c.el" "d.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-character-set-range ()
|
||||
"Test \"[...]\" for character ranges."
|
||||
(with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "[a-c].el")
|
||||
'("a.el" "b.el" "c.el")))
|
||||
(should (equal (eshell-extended-glob "[^a-c].el")
|
||||
'("d.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-character-set-class ()
|
||||
"Test \"[...]\" for character classes."
|
||||
(with-fake-files '("1.el" "a.el" "b.el" "c.el" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "[[:alpha:]].el")
|
||||
'("a.el" "b.el" "c.el")))
|
||||
(should (equal (eshell-extended-glob "[^[:alpha:]].el")
|
||||
'("1.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-character-set-mixed ()
|
||||
"Test \"[...]\" with multiple kinds of members at once."
|
||||
(with-fake-files '("1.el" "a.el" "b.el" "c.el" "d.el" "dir/a.el")
|
||||
(should (equal (eshell-extended-glob "[ac-d[:digit:]].el")
|
||||
'("1.el" "a.el" "c.el" "d.el")))
|
||||
(should (equal (eshell-extended-glob "[^ac-d[:digit:]].el")
|
||||
'("b.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-group-alternative ()
|
||||
"Test \"(x|y)\" matches either \"x\" or \"y\"."
|
||||
(with-fake-files '("em-alias.el" "em-banner.el" "esh-arg.el" "misc.el"
|
||||
"test/em-xtra.el")
|
||||
(should (equal (eshell-extended-glob "e(m|sh)-*.el")
|
||||
'("em-alias.el" "em-banner.el" "esh-arg.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-n-or-more-characters ()
|
||||
"Test that \"x#\" and \"x#\" match zero or more instances of \"x\"."
|
||||
(with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
|
||||
(should (equal (eshell-extended-glob "hi#.el")
|
||||
'("h.el" "hi.el" "hii.el")))
|
||||
(should (equal (eshell-extended-glob "hi##.el")
|
||||
'("hi.el" "hii.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-n-or-more-groups ()
|
||||
"Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"."
|
||||
(with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
|
||||
(should (equal (eshell-extended-glob "hi#.el")
|
||||
'("h.el" "hi.el" "hii.el")))
|
||||
(should (equal (eshell-extended-glob "hi##.el")
|
||||
'("hi.el" "hii.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-n-or-more-character-sets ()
|
||||
"Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"."
|
||||
(with-fake-files '("w.el" "wh.el" "wha.el" "whi.el" "whaha.el" "dir/wha.el")
|
||||
(should (equal (eshell-extended-glob "w[ah]#.el")
|
||||
'("w.el" "wh.el" "wha.el" "whaha.el")))
|
||||
(should (equal (eshell-extended-glob "w[ah]##.el")
|
||||
'("wh.el" "wha.el" "whaha.el")))))
|
||||
|
||||
(ert-deftest em-glob-test/match-x-but-not-y ()
|
||||
"Test that \"x~y\" matches \"x\" but not \"y\"."
|
||||
(with-fake-files '("1" "12" "123" "42" "dir/1")
|
||||
(should (equal (eshell-extended-glob "[[:digit:]]##~4?")
|
||||
'("1" "12" "123")))))
|
||||
|
||||
(ert-deftest em-glob-test/no-matches ()
|
||||
"Test behavior when a glob fails to match any files."
|
||||
(with-fake-files '("foo.el" "bar.el")
|
||||
(should (equal (eshell-extended-glob "*.txt")
|
||||
"*.txt"))
|
||||
(let ((eshell-error-if-no-glob t))
|
||||
(should-error (eshell-extended-glob "*.txt")))))
|
||||
|
||||
;; em-glob-tests.el ends here
|
||||
Loading…
Add table
Add a link
Reference in a new issue