]> git.eshelyaron.com Git - emacs.git/commitdiff
Add unit tests and documentation for Eshell pattern-based globs
authorJim Porter <jporterbugs@gmail.com>
Wed, 9 Mar 2022 01:07:26 +0000 (17:07 -0800)
committerEli Zaretskii <eliz@gnu.org>
Sun, 17 Apr 2022 07:27:39 +0000 (10:27 +0300)
* 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.

doc/misc/eshell.texi
lisp/eshell/em-glob.el
test/lisp/eshell/em-glob-tests.el [new file with mode: 0644]

index 372e4c3ffbd5b268834ec0dd46a9ab8f3df64fd1..648917f62d1f6961508fe4bd45fb4a7678a2c43b 100644 (file)
@@ -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
index 842f27a4920f806e2909134b8c381270fad2bf7f..52531ff8939d2d0653b04528c27bc8a4501f755c 100644 (file)
@@ -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 (file)
index 0000000..9976b32
--- /dev/null
@@ -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