aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJim Porter2022-03-08 17:07:26 -0800
committerEli Zaretskii2022-04-17 10:27:39 +0300
commitbbb92dde01ec3fc46b24247fb2d181a21dbcc40a (patch)
tree46c8eaede0f5d432d447fefb768338f9e847ef4a
parent265f4ef70233c4708cbbdeb1850541570c40fdd6 (diff)
downloademacs-bbb92dde01ec3fc46b24247fb2d181a21dbcc40a.tar.gz
emacs-bbb92dde01ec3fc46b24247fb2d181a21dbcc40a.zip
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.
1089 1089
1090@node Globbing 1090@node Globbing
1091@section Globbing 1091@section Globbing
1092Eshell's globbing syntax is very similar to that of Zsh. Users coming 1092@vindex eshell-glob-case-insensitive
1093from Bash can still use Bash-style globbing, as there are no 1093Eshell's globbing syntax is very similar to that of Zsh
1094incompatibilities. Most globbing is pattern-based expansion, but there 1094(@pxref{Filename Generation, , , zsh, The Z Shell Manual}). Users
1095is also predicate-based expansion. @xref{Filename Generation, , , 1095coming from Bash can still use Bash-style globbing, as there are no
1096zsh, The Z Shell Manual}, 1096incompatibilities.
1097for full syntax. To customize the syntax and behavior of globbing in 1097
1098Eshell see the Customize@footnote{@xref{Easy Customization, , , emacs, 1098By default, globs are case sensitive, except on MS-DOS/MS-Windows
1099The GNU Emacs Manual}.} 1099systems. You can control this behavior via the
1100groups ``eshell-glob'' and ``eshell-pred''. 1100@code{eshell-glob-case-insensitive} option. You can further customize
1101the syntax and behavior of globbing in Eshell via the Customize group
1102``eshell-glob'' (@pxref{Easy Customization, , , emacs, The GNU Emacs
1103Manual}).
1104
1105@table @samp
1106
1107@item *
1108Matches any string (including the empty string). For example,
1109@samp{*.el} matches any file with the @file{.el} extension.
1110
1111@item ?
1112Matches any single character. For example, @samp{?at} matches
1113@file{cat} and @file{bat}, but not @file{goat}.
1114
1115@item **/
1116Matches zero or more subdirectories in a file name. For example,
1117@samp{**/foo.el} matches @file{foo.el}, @file{bar/foo.el},
1118@file{bar/baz/foo.el}, etc. Note that this cannot be combined with
1119any other patterns in the same file name segment, so while
1120@samp{foo/**/bar.el} is allowed, @samp{foo**/bar.el} is not.
1121
1122@item ***/
1123Like @samp{**/}, but follows symlinks as well.
1124
1125@cindex character sets, in Eshell glob patterns
1126@cindex character classes, in Eshell glob patterns
1127@item [ @dots{} ]
1128Defines a @dfn{character set} (@pxref{Regexps, , , emacs, The GNU
1129Emacs Manual}). A character set matches characters between the two
1130brackets; for example, @samp{[ad]} matches @file{a} and @file{d}. You
1131can also include ranges of characters in the set by separating the
1132start and end with @samp{-}. Thus, @samp{[a-z]} matches any
1133lower-case @acronym{ASCII} letter. Note that, unlike in Zsh,
1134character ranges are interpreted in the Unicode codepoint order, not
1135in the locale-dependent collation order.
1136
1137Additionally, you can include @dfn{character classes} in a character
1138set. A @samp{[:} and balancing @samp{:]} enclose a character class
1139inside a character set. For instance, @samp{[[:alnum:]]}
1140matches any letter or digit. @xref{Char Classes, , , elisp, The Emacs
1141Lisp Reference Manual}, for a list of character classes.
1142
1143@cindex complemented character sets, in Eshell glob patterns
1144@item [^ @dots{} ]
1145Defines a @dfn{complemented character set}. This behaves just like a
1146character set, but matches any character @emph{except} the ones
1147specified.
1148
1149@cindex groups, in Eshell glob patterns
1150@item ( @dots{} )
1151Defines a @dfn{group}. A group matches the pattern between @samp{(}
1152and @samp{)}. Note that a group can only match a single file name
1153component, so a @samp{/} inside a group will signal an error.
1154
1155@item @var{x}|@var{y}
1156Inside of a group, matches either @var{x} or @var{y}. For example,
1157@samp{e(m|sh)-*} matches any file beginning with @file{em-} or
1158@file{esh-}.
1159
1160@item @var{x}#
1161Matches zero or more copies of the glob pattern @var{x}. For example,
1162@samp{fo#.el} matches @file{f.el}, @file{fo.el}, @file{foo.el}, etc.
1163
1164@item @var{x}##
1165Matches one or more copies of the glob pattern @var{x}. Thus,
1166@samp{fo#.el} matches @file{fo.el}, @file{foo.el}, @file{fooo.el},
1167etc.
1168
1169@item @var{x}~@var{y}
1170Matches anything that matches the pattern @var{x} but not @var{y}. For
1171example, @samp{[[:digit:]]#~4?} matches @file{1} and @file{12}, but
1172not @file{42}. Note that unlike in Zsh, only a single @samp{~}
1173operator can be used in a pattern, and it cannot be inside of a group
1174like @samp{(@var{x}~@var{y})}.
1175
1176@end table
1101 1177
1102@node Input/Output 1178@node Input/Output
1103@chapter Input/Output 1179@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."
233 "\\'"))) 233 "\\'")))
234 234
235(defun eshell-extended-glob (glob) 235(defun eshell-extended-glob (glob)
236 "Return a list of files generated from GLOB, perhaps looking for DIRS-ONLY. 236 "Return a list of files matched by GLOB.
237If no files match, signal an error (if `eshell-error-if-no-glob'
238is non-nil), or otherwise return GLOB itself.
239
237This function almost fully supports zsh style filename generation 240This function almost fully supports zsh style filename generation
238syntax. Things that are not supported are: 241syntax. Things that are not supported are:
239 242
@@ -243,12 +246,7 @@ syntax. Things that are not supported are:
243 foo~x(a|b) (a|b) will be interpreted as a predicate/modifier list 246 foo~x(a|b) (a|b) will be interpreted as a predicate/modifier list
244 247
245Mainly they are not supported because file matching is done with Emacs 248Mainly they are not supported because file matching is done with Emacs
246regular expressions, and these cannot support the above constructs. 249regular expressions, and these cannot support the above constructs."
247
248If this routine fails, it returns nil. Otherwise, it returns a list
249the form:
250
251 (INCLUDE-REGEXP EXCLUDE-REGEXP (PRED-FUNC-LIST) (MOD-FUNC-LIST))"
252 (let ((paths (eshell-split-path glob)) 250 (let ((paths (eshell-split-path glob))
253 eshell-glob-matches message-shown) 251 eshell-glob-matches message-shown)
254 (unwind-protect 252 (unwind-protect
@@ -287,7 +285,7 @@ the form:
287 glob (car globs) 285 glob (car globs)
288 len (length glob))))) 286 len (length glob)))))
289 (if (and recurse-p (not glob)) 287 (if (and recurse-p (not glob))
290 (error "`**' cannot end a globbing pattern")) 288 (error "`**/' cannot end a globbing pattern"))
291 (let ((index 1)) 289 (let ((index 1))
292 (setq incl glob) 290 (setq incl glob)
293 (while (and (eq incl glob) 291 (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 @@
1;;; em-glob-tests.el --- em-glob test suite -*- lexical-binding:t -*-
2
3;; Copyright (C) 2022 Free Software Foundation, Inc.
4
5;; This file is part of GNU Emacs.
6
7;; GNU Emacs is free software: you can redistribute it and/or modify
8;; it under the terms of the GNU General Public License as published by
9;; the Free Software Foundation, either version 3 of the License, or
10;; (at your option) any later version.
11
12;; GNU Emacs is distributed in the hope that it will be useful,
13;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;; GNU General Public License for more details.
16
17;; You should have received a copy of the GNU General Public License
18;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
19
20;;; Commentary:
21
22;; Tests for Eshell's glob expansion.
23
24;;; Code:
25
26(require 'ert)
27(require 'em-glob)
28
29(defmacro with-fake-files (files &rest body)
30 "Evaluate BODY forms, pretending that FILES exist on the filesystem.
31FILES is a list of file names that should be reported as
32appropriate by `file-name-all-completions'. Any file name
33component ending in \"symlink\" is treated as a symbolic link."
34 (declare (indent 1))
35 `(cl-letf (((symbol-function 'file-name-all-completions)
36 (lambda (file directory)
37 (cl-assert (string= file ""))
38 (setq directory (expand-file-name directory))
39 `("./" "../"
40 ,@(delete-dups
41 (remq nil
42 (mapcar
43 (lambda (file)
44 (setq file (expand-file-name file))
45 (when (string-prefix-p directory file)
46 (replace-regexp-in-string
47 "/.*" "/"
48 (substring file (length directory)))))
49 ,files))))))
50 ((symbol-function 'file-symlink-p)
51 (lambda (file)
52 (string-suffix-p "symlink" file))))
53 ,@body))
54
55;;; Tests:
56
57(ert-deftest em-glob-test/match-any-string ()
58 "Test that \"*\" pattern matches any string."
59 (with-fake-files '("a.el" "b.el" "c.txt" "dir/a.el")
60 (should (equal (eshell-extended-glob "*.el")
61 '("a.el" "b.el")))))
62
63(ert-deftest em-glob-test/match-any-character ()
64 "Test that \"?\" pattern matches any character."
65 (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el")
66 (should (equal (eshell-extended-glob "?.el")
67 '("a.el" "b.el")))))
68
69(ert-deftest em-glob-test/match-recursive ()
70 "Test that \"**/\" recursively matches directories."
71 (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
72 "dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
73 (should (equal (eshell-extended-glob "**/a.el")
74 '("a.el" "dir/a.el" "dir/sub/a.el")))))
75
76(ert-deftest em-glob-test/match-recursive-follow-symlinks ()
77 "Test that \"***/\" recursively matches directories, following symlinks."
78 (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
79 "dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
80 (should (equal (eshell-extended-glob "***/a.el")
81 '("a.el" "dir/a.el" "dir/sub/a.el" "dir/symlink/a.el"
82 "symlink/a.el" "symlink/sub/a.el")))))
83
84(ert-deftest em-glob-test/match-recursive-mixed ()
85 "Test combination of \"**/\" and \"***/\"."
86 (with-fake-files '("dir/a.el" "dir/sub/a.el" "dir/sub2/a.el"
87 "dir/symlink/a.el" "dir/sub/symlink/a.el" "symlink/a.el"
88 "symlink/sub/a.el" "symlink/sub/symlink/a.el")
89 (should (equal (eshell-extended-glob "**/sub/***/a.el")
90 '("dir/sub/a.el" "dir/sub/symlink/a.el")))
91 (should (equal (eshell-extended-glob "***/sub/**/a.el")
92 '("dir/sub/a.el" "symlink/sub/a.el")))))
93
94(ert-deftest em-glob-test/match-character-set-individual ()
95 "Test \"[...]\" for individual characters."
96 (with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
97 (should (equal (eshell-extended-glob "[ab].el")
98 '("a.el" "b.el")))
99 (should (equal (eshell-extended-glob "[^ab].el")
100 '("c.el" "d.el")))))
101
102(ert-deftest em-glob-test/match-character-set-range ()
103 "Test \"[...]\" for character ranges."
104 (with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
105 (should (equal (eshell-extended-glob "[a-c].el")
106 '("a.el" "b.el" "c.el")))
107 (should (equal (eshell-extended-glob "[^a-c].el")
108 '("d.el")))))
109
110(ert-deftest em-glob-test/match-character-set-class ()
111 "Test \"[...]\" for character classes."
112 (with-fake-files '("1.el" "a.el" "b.el" "c.el" "dir/a.el")
113 (should (equal (eshell-extended-glob "[[:alpha:]].el")
114 '("a.el" "b.el" "c.el")))
115 (should (equal (eshell-extended-glob "[^[:alpha:]].el")
116 '("1.el")))))
117
118(ert-deftest em-glob-test/match-character-set-mixed ()
119 "Test \"[...]\" with multiple kinds of members at once."
120 (with-fake-files '("1.el" "a.el" "b.el" "c.el" "d.el" "dir/a.el")
121 (should (equal (eshell-extended-glob "[ac-d[:digit:]].el")
122 '("1.el" "a.el" "c.el" "d.el")))
123 (should (equal (eshell-extended-glob "[^ac-d[:digit:]].el")
124 '("b.el")))))
125
126(ert-deftest em-glob-test/match-group-alternative ()
127 "Test \"(x|y)\" matches either \"x\" or \"y\"."
128 (with-fake-files '("em-alias.el" "em-banner.el" "esh-arg.el" "misc.el"
129 "test/em-xtra.el")
130 (should (equal (eshell-extended-glob "e(m|sh)-*.el")
131 '("em-alias.el" "em-banner.el" "esh-arg.el")))))
132
133(ert-deftest em-glob-test/match-n-or-more-characters ()
134 "Test that \"x#\" and \"x#\" match zero or more instances of \"x\"."
135 (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
136 (should (equal (eshell-extended-glob "hi#.el")
137 '("h.el" "hi.el" "hii.el")))
138 (should (equal (eshell-extended-glob "hi##.el")
139 '("hi.el" "hii.el")))))
140
141(ert-deftest em-glob-test/match-n-or-more-groups ()
142 "Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"."
143 (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
144 (should (equal (eshell-extended-glob "hi#.el")
145 '("h.el" "hi.el" "hii.el")))
146 (should (equal (eshell-extended-glob "hi##.el")
147 '("hi.el" "hii.el")))))
148
149(ert-deftest em-glob-test/match-n-or-more-character-sets ()
150 "Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"."
151 (with-fake-files '("w.el" "wh.el" "wha.el" "whi.el" "whaha.el" "dir/wha.el")
152 (should (equal (eshell-extended-glob "w[ah]#.el")
153 '("w.el" "wh.el" "wha.el" "whaha.el")))
154 (should (equal (eshell-extended-glob "w[ah]##.el")
155 '("wh.el" "wha.el" "whaha.el")))))
156
157(ert-deftest em-glob-test/match-x-but-not-y ()
158 "Test that \"x~y\" matches \"x\" but not \"y\"."
159 (with-fake-files '("1" "12" "123" "42" "dir/1")
160 (should (equal (eshell-extended-glob "[[:digit:]]##~4?")
161 '("1" "12" "123")))))
162
163(ert-deftest em-glob-test/no-matches ()
164 "Test behavior when a glob fails to match any files."
165 (with-fake-files '("foo.el" "bar.el")
166 (should (equal (eshell-extended-glob "*.txt")
167 "*.txt"))
168 (let ((eshell-error-if-no-glob t))
169 (should-error (eshell-extended-glob "*.txt")))))
170
171;; em-glob-tests.el ends here