summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJim Porter <jporterbugs@gmail.com>2022-03-08 17:07:26 -0800
committerEli Zaretskii <eliz@gnu.org>2022-04-17 10:27:39 +0300
commitbbb92dde01ec3fc46b24247fb2d181a21dbcc40a (patch)
tree46c8eaede0f5d432d447fefb768338f9e847ef4a
parent265f4ef70233c4708cbbdeb1850541570c40fdd6 (diff)
downloademacs-bbb92dde01ec3fc46b24247fb2d181a21dbcc40a.tar.gz
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.
-rw-r--r--doc/misc/eshell.texi94
-rw-r--r--lisp/eshell/em-glob.el14
-rw-r--r--test/lisp/eshell/em-glob-tests.el171
3 files changed, 262 insertions, 17 deletions
diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 372e4c3ffbd..648917f62d1 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -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
diff --git a/lisp/eshell/em-glob.el b/lisp/eshell/em-glob.el
index 842f27a4920..52531ff8939 100644
--- a/lisp/eshell/em-glob.el
+++ b/lisp/eshell/em-glob.el
@@ -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)
diff --git a/test/lisp/eshell/em-glob-tests.el b/test/lisp/eshell/em-glob-tests.el
new file mode 100644
index 00000000000..9976b32ffe7
--- /dev/null
+++ b/test/lisp/eshell/em-glob-tests.el
@@ -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