]> git.eshelyaron.com Git - emacs.git/commitdiff
Allow splicing Eshell globs in-place
authorJim Porter <jporterbugs@gmail.com>
Thu, 17 Aug 2023 19:23:26 +0000 (12:23 -0700)
committerJim Porter <jporterbugs@gmail.com>
Mon, 21 Aug 2023 18:42:46 +0000 (11:42 -0700)
This means that Eshell globs can now expand the same way as if the
user had typed each matching file individually.

* lisp/eshell/em-glob.el (eshell-glob-splice-results): New option.
(eshell-no-command-globbing, eshell-add-glob-modifier): Handle spliced
globs.
(eshell-extended-glob): Always return a list when splicing.

* lisp/eshell/em-pred.el (eshell-parse-arg-modifier): Ensure
'eshell-splice-args' is always at the end of the list of modifiers if
present.

* test/lisp/eshell/em-glob-tests.el
(em-glob-test/expand/splice-results)
(em-glob-test/expand/no-splice-results)
(em-glob-test/expand/explicitly-splice-results)
(em-glob-test/expand/explicitly-listify-results): New tests.
(em-glob-test/no-matches): Check result when
'eshell-glob-splice-results' is nil/non-nil.

* doc/misc/eshell.texi (Arguments): Expand explanation about argument
flattening.
(Globbing): Document splicing behavior of globs.

* etc/NEWS: Announce this change.

doc/misc/eshell.texi
etc/NEWS
lisp/eshell/em-glob.el
lisp/eshell/em-pred.el
test/lisp/eshell/em-glob-tests.el

index 6890728a81d609e6d6e3fdf990fe6c460779f89c..59c0745715852896f4c1ac287d7a399084309a47 100644 (file)
@@ -317,9 +317,10 @@ specify an argument of some other data type, you can use a Lisp form
 (1 2 3)
 @end example
 
-Additionally, many built-in Eshell commands (@pxref{Built-ins}) will
-flatten the arguments they receive, so passing a list as an argument
-will ``spread'' the elements into multiple arguments:
+When calling external commands (and many built-in Eshell commands,
+too) Eshell will flatten the arguments the command receives, so
+passing a list as an argument will ``spread'' the elements into
+multiple arguments:
 
 @example
 ~ $ printnl (list 1 2) 3
@@ -1466,18 +1467,28 @@ other arguments around it.  For example, if @var{numbers} is the list
 
 @node Globbing
 @section Globbing
-@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
+@vindex eshell-glob-case-insensitive
+Globs are case sensitive by default, 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
-@code{eshell-glob} (@pxref{Easy Customization, , , emacs, The GNU
-Emacs Manual}).
+@code{eshell-glob-case-insensitive} option.
+
+@vindex eshell-glob-splice-results
+By default, Eshell expands the results of a glob as a sublist into the
+list of arguments.  You can change this to splice the results in-place
+by setting @code{eshell-glob-splice-results} to a non-@code{nil}
+value.  If you want to splice a glob in-place for just one use, you
+can use a subcommand form like @samp{$@@@{listify @var{my-glob}@}}.
+(Conversely, you can explicitly expand a glob as a sublist via
+@samp{$@{listify @var{my-glob}@}}.)
+
+You can further customize the syntax and behavior of globbing in
+Eshell via the Customize group @code{eshell-glob} (@pxref{Easy
+Customization, , , emacs, The GNU Emacs Manual}).
 
 @table @samp
 
@@ -2386,8 +2397,6 @@ be Eshell's job?
 This would be so that if a Lisp function calls @code{print}, everything
 will happen as it should (albeit slowly).
 
-@item If a globbing pattern returns one match, should it be a list?
-
 @item Make sure syntax table is correct in Eshell mode
 
 So that @kbd{M-@key{DEL}} acts in a predictable manner, etc.
index c97df11042d2709a1c93a017de156169ebc9855d..66a5fcf6a62380d9df5ea26e9ba41a99594c2c90 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -312,6 +312,13 @@ of arguments into a command, such as when defining aliases.  For more
 information, see the "(eshell) Dollars Expansion" node in the Eshell
 manual.
 
++++
+*** You can now splice Eshell globs in-place into argument lists.
+By setting 'eshell-glob-splice-results' to a non-nil value, Eshell
+will expand glob results in-place as if you had typed each matching
+file name individually.  For more information, see the "(eshell)
+Globbing" node in the Eshell manual.
+
 +++
 *** Eshell now supports negative numbers and ranges for indices.
 Now, you can retrieve the last element of a list with '$my-list[-1]'
index d00f8c93cd12854ada25ca707bdcc4c5495038c3..1141b673e97b30e12c6d1d4a317e39b41ddbfda0 100644 (file)
@@ -69,6 +69,15 @@ by zsh for filename generation."
   :type 'hook
   :group 'eshell-glob)
 
+(defcustom eshell-glob-splice-results nil
+  "If non-nil, the results of glob patterns will be spliced in-place.
+When splicing, the resulting command is as though the user typed
+each result individually.  Otherwise, the glob results a single
+argument as a list."
+  :version "30.1"
+  :type 'boolean
+  :group 'eshell-glob)
+
 (defcustom eshell-glob-include-dot-files nil
   "If non-nil, glob patterns will match files beginning with a dot."
   :type 'boolean
@@ -139,12 +148,15 @@ This mimics the behavior of zsh if non-nil, but bash if nil."
 (defun eshell-no-command-globbing (terms)
   "Don't glob the command argument.  Reflect this by modifying TERMS."
   (ignore
-   (when (and (listp (car terms))
-             (eq (caar terms) 'eshell-extended-glob))
-     (setcar terms (cadr (car terms))))))
+   (pcase (car terms)
+     ((or `(eshell-extended-glob ,term)
+          `(eshell-splice-args (eshell-extended-glob ,term)))
+      (setcar terms term)))))
 
 (defun eshell-add-glob-modifier ()
   "Add `eshell-extended-glob' to the argument modifier list."
+  (when eshell-glob-splice-results
+    (add-to-list 'eshell-current-modifiers 'eshell-splice-args t))
   (add-to-list 'eshell-current-modifiers 'eshell-extended-glob))
 
 (defun eshell-parse-glob-chars ()
@@ -326,7 +338,9 @@ regular expressions, and these cannot support the above constructs."
     (or (and eshell-glob-matches (sort eshell-glob-matches #'string<))
        (if eshell-error-if-no-glob
            (error "No matches found: %s" glob)
-         glob))))
+          (if eshell-glob-splice-results
+              (list glob)
+            glob)))))
 
 ;; FIXME does this really need to abuse eshell-glob-matches, message-shown?
 (defun eshell-glob-entries (path globs only-dirs)
index bfb0dad60ef37ec4b3ef1b94cca3eaaa971831e5..1d67f1af990893ba4de377714b8af8cf52a726e4 100644 (file)
@@ -301,16 +301,25 @@ This function is specially for adding onto `eshell-parse-argument-hook'."
                    (modifiers (eshell-parse-modifiers))
                   (preds (car modifiers))
                   (mods (cdr modifiers)))
-             (if (or preds mods)
-                 ;; has to go at the end, which is only natural since
-                 ;; syntactically it can only occur at the end
-                 (setq eshell-current-modifiers
-                       (append
-                        eshell-current-modifiers
-                        (list
-                         (lambda (lst)
-                           (eshell-apply-modifiers
-                            lst preds mods modifier-string))))))))
+              (when (or preds mods)
+                ;; Has to go at the end, which is only natural since
+                ;; syntactically it can only occur at the end.
+                (setq eshell-current-modifiers
+                      (append
+                       eshell-current-modifiers
+                       (list
+                        (lambda (lst)
+                          (eshell-apply-modifiers
+                           lst preds mods modifier-string)))))
+                (when (memq 'eshell-splice-args eshell-current-modifiers)
+                  ;; If splicing results, ensure that
+                  ;; `eshell-splice-args' is the last modifier.
+                  ;; Eshell's command parsing can't handle it anywhere
+                  ;; else.
+                  (setq eshell-current-modifiers
+                        (append (delq 'eshell-splice-args
+                                      eshell-current-modifiers)
+                                (list 'eshell-splice-args)))))))
          (goto-char (1+ end))
          (eshell-finish-arg))))))
 
index c33af88a3748006741aea6d61297a23fd596247a..6e07225657cbadd5cfabe5b785ae941d7c785c4d 100644 (file)
 (require 'ert)
 (require 'em-glob)
 
+(require 'eshell-tests-helpers
+         (expand-file-name "eshell-tests-helpers"
+                           (file-name-directory (or load-file-name
+                                                    default-directory))))
+
+(defvar eshell-prefer-lisp-functions)
+
 (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
@@ -54,6 +61,60 @@ component ending in \"symlink\" is treated as a symbolic link."
 
 ;;; Tests:
 
+(ert-deftest em-glob-test/expand/splice-results ()
+  "Test that globs are spliced into the argument list when
+`eshell-glob-splice-results' is non-nil."
+  (let ((eshell-prefer-lisp-functions t)
+        (eshell-glob-splice-results t))
+    (with-fake-files '("a.el" "b.el" "c.txt")
+      ;; Ensure the default expansion splices the glob.
+      (eshell-command-result-equal "list *.el" '("a.el" "b.el"))
+      (eshell-command-result-equal "list *.txt" '("c.txt"))
+      (eshell-command-result-equal "list *.no" '("*.no")))))
+
+(ert-deftest em-glob-test/expand/no-splice-results ()
+  "Test that globs are treated as lists when
+`eshell-glob-splice-results' is nil."
+  (let ((eshell-prefer-lisp-functions t)
+        (eshell-glob-splice-results nil))
+    (with-fake-files '("a.el" "b.el" "c.txt")
+      ;; Ensure the default expansion splices the glob.
+      (eshell-command-result-equal "list *.el" '(("a.el" "b.el")))
+      (eshell-command-result-equal "list *.txt" '(("c.txt")))
+      ;; The no-matches case is special here: the glob is just the
+      ;; string, not the list of results.
+      (eshell-command-result-equal "list *.no" '("*.no")))))
+
+(ert-deftest em-glob-test/expand/explicitly-splice-results ()
+  "Test explicitly splicing globs works the same no matter the
+value of `eshell-glob-splice-results'."
+  (let ((eshell-prefer-lisp-functions t))
+    (dolist (eshell-glob-splice-results '(nil t))
+      (ert-info ((format "eshell-glob-splice-results: %s"
+                         eshell-glob-splice-results))
+        (with-fake-files '("a.el" "b.el" "c.txt")
+          (eshell-command-result-equal "list $@{listify *.el}"
+                                       '("a.el" "b.el"))
+          (eshell-command-result-equal "list $@{listify *.txt}"
+                                       '("c.txt"))
+          (eshell-command-result-equal "list $@{listify *.no}"
+                                       '("*.no")))))))
+
+(ert-deftest em-glob-test/expand/explicitly-listify-results ()
+  "Test explicitly listifying globs works the same no matter the
+value of `eshell-glob-splice-results'."
+  (let ((eshell-prefer-lisp-functions t))
+    (dolist (eshell-glob-splice-results '(nil t))
+      (ert-info ((format "eshell-glob-splice-results: %s"
+                         eshell-glob-splice-results))
+        (with-fake-files '("a.el" "b.el" "c.txt")
+          (eshell-command-result-equal "list ${listify *.el}"
+                                       '(("a.el" "b.el")))
+          (eshell-command-result-equal "list ${listify *.txt}"
+                                       '(("c.txt")))
+          (eshell-command-result-equal "list ${listify *.no}"
+                                       '(("*.no"))))))))
+
 (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")
@@ -191,6 +252,9 @@ component ending in \"symlink\" is treated as a symbolic link."
   (with-fake-files '("foo.el" "bar.el")
     (should (equal (eshell-extended-glob "*.txt")
                    "*.txt"))
+    (let ((eshell-glob-splice-results t))
+      (should (equal (eshell-extended-glob "*.txt")
+                     '("*.txt"))))
     (let ((eshell-error-if-no-glob t))
       (should-error (eshell-extended-glob "*.txt")))))