Subject: bug#54603: 29.0.50; [PATCH] Eshell's external pipe module interferes with other argument parsing hooks

From: Jim Porter <[email-masked]>
Date: Sun, 27 Mar 2022 19:21:17 -0700

From "emacs -Q --eval '(eshell)':

   ~ $ (eq 'foo nil)
   Expecting completion of delimiter ' ...

   M-: (unload-feature 'em-extpipe)

   ~ $ (eq 'foo nil)
   ;; No output, since it returns nil. This is correct.

(As an ironic twist, you'd normally be able to run "(unload-feature 
'em-extpipe)" as a command in Eshell, but this bug prevents that from 
working.)

I don't fully understand all the details of the code in 
lisp/eshell/em-extpipe.el yet, but I think this is the culprit:

                          (while (or (eshell-parse-lisp-argument)
                                     (eshell-parse-backslash)
                                     (eshell-parse-double-quote)
                                     (eshell-parse-literal-quote)))

In particular, since there are 3 single-quotes, 
`eshell-parse-literal-quote' throws an `eshell-incomplete' error, which 
gums up the works.

The attached patch resolves the issue for me, but I'm not sure if it's 
the best strategy. If possible, I think it would be better for 
`eshell-parse-external-pipeline' to solely focus on finding the external 
pipe operators ("*|", "*<", and "*>")[1] and then for 
`eshell-rewrite-external-pipeline' to prepare the command string to pass 
to sh. This would also have the advantage[2] of making it possible to 
support a richer set of Eshell features with external pipes, such as the 
following:

   ~ $ echo $(message "[%s]" "hi") *| cat
   zsh:1: command not found: message

(If you remove the "*", this outputs "[hi]", and it should be 
technically possible to make this work with external pipes too, provided 
it executes the Lisp code before generating the command string for sh.)

[1] See `eshell-parse-delimiter' for an example of how that might be 
implemented.
[2] Well, maybe there's room for debate on what the right behavior here is.
From fab48a28ff80f1c74f2d50d4f69edb056f88d119 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Sun, 27 Mar 2022 19:04:42 -0700
Subject: [PATCH] Make Eshell's extpipe more lenient when looking for its
 operators

This could cause some errors when executing Eshell commands with an
odd number of quotation marks, such as "(eq 'foo nil)".

* lisp/eshell/em-extpipe.el (eshell-parse-external-pipeline): Catch
'eshell-incomplete' errors.

* test/lisp/eshell/eshell-tests.el
(eshell-test/lisp-command-with-quote): New test.
---
 lisp/eshell/em-extpipe.el        | 9 +++++----
 test/lisp/eshell/eshell-tests.el | 4 ++++
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el
index eb5b3bfe1d..e7b421982a 100644
--- a/lisp/eshell/em-extpipe.el
+++ b/lisp/eshell/em-extpipe.el
@@ -105,10 +105,11 @@ eshell-parse-external-pipeline
                        (if (re-search-forward pat next t)
                            (throw 'found (match-beginning 1))
                          (goto-char next)
-                         (while (or (eshell-parse-lisp-argument)
-                                    (eshell-parse-backslash)
-                                    (eshell-parse-double-quote)
-                                    (eshell-parse-literal-quote)))
+                         (catch 'eshell-incomplete
+                           (while (or (eshell-parse-lisp-argument)
+                                      (eshell-parse-backslash)
+                                      (eshell-parse-double-quote)
+                                      (eshell-parse-literal-quote))))
                          ;; Guard against an infinite loop if none of
                          ;; the parsers moved us forward.
                          (unless (or (> (point) next) (eobp))
diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el
index e31db07c61..1e303f70e5 100644
--- a/test/lisp/eshell/eshell-tests.el
+++ b/test/lisp/eshell/eshell-tests.el
@@ -44,6 +44,10 @@ eshell-test/lisp-command
   "Test `eshell-command-result' with an elisp command."
   (should (equal (eshell-test-command-result "(+ 1 2)") 3)))
 
+(ert-deftest eshell-test/lisp-command-with-quote ()
+  "Test `eshell-command-result' with an elisp command containing a quote."
+  (should (equal (eshell-test-command-result "(eq 'foo nil)") nil)))
+
 (ert-deftest eshell-test/for-loop ()
   "Test `eshell-command-result' with a for loop.."
   (let ((process-environment (cons "foo" process-environment)))
-- 
2.25.1