Subject: 29.0.50; [PATCH] Improve Eshell's logic for finding delimiters in arguments

From: Jim Porter <[email-masked]>
Date: Sat, 2 Apr 2022 10:30:10 -0700

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

   ;; Good:
   ~ $ echo "\\foo"
   \foo

   ;; Bad:
   ~ $ echo "\\"
   Expecting completion of delimiter " ...

This is an issue with the logic for `eshell-find-delimiter'. When seeing 
the first '\', it didn't recognize that the second '\' was being 
escaped. Then, it looked at the second '\' and saw that it preceded a 
'"', thus thinking that the '"' was escaped and the string was 
unterminated. The fix is that `eshell-find-delimiter' should treat '\' 
as an escapable character when using backslash escapes.

In addition, I fixed the docstring. Previously, it said that backslash 
escaping only occurred when BACKSLASH-P was non-nil *and* the opening 
and closing delimiters are the same. However, when the opening and 
closing delimiters are different, backslash escaping is always used.
From 566a1d7eeab6a1fa2babc54609f96e441cd45efc Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Sun, 27 Mar 2022 12:09:58 -0700
Subject: [PATCH] Fix handling of '\\' inside double-quotes in Eshell

Previously, Eshell would get confused and think the following command
was unterminated due to the second double-quote looking like it was
escaped:

  echo "\\"

* lisp/eshell/esh-util.el (eshell-find-delimiter): Correct docstring
and treat '\' as an escapeable character when using backslash escapes.

* test/lisp/eshell/eshell-tests.el
(eshell-test/escape-special-quoted): Adapt test.
---
 lisp/eshell/esh-util.el          | 51 +++++++++++++++++---------------
 test/lisp/eshell/eshell-tests.el |  4 +--
 2 files changed, 29 insertions(+), 26 deletions(-)

diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el
index 788404fc43..8089d4d74b 100644
--- a/lisp/eshell/esh-util.el
+++ b/lisp/eshell/esh-util.el
@@ -151,49 +151,52 @@ eshell-condition-case
 (defun eshell-find-delimiter
   (open close &optional bound reverse-p backslash-p)
   "From point, find the CLOSE delimiter corresponding to OPEN.
-The matching is bounded by BOUND.
-If REVERSE-P is non-nil, process the region backwards.
-If BACKSLASH-P is non-nil, and OPEN and CLOSE are the same character,
-then quoting is done by a backslash, rather than a doubled delimiter."
+The matching is bounded by BOUND. If REVERSE-P is non-nil,
+process the region backwards.
+
+If BACKSLASH-P is non-nil, or OPEN and CLOSE are different
+characters, then a backslash can be used to escape a delimiter
+(or another backslash).  Otherwise, the delimiter is escaped by
+doubling it up."
   (save-excursion
     (let ((depth 1)
 	  (bound (or bound (point-max))))
-      (if (if reverse-p
-	      (eq (char-before) close)
-	    (eq (char-after) open))
-	  (forward-char (if reverse-p -1 1)))
+      (when (if reverse-p
+                (eq (char-before) close)
+              (eq (char-after) open))
+        (forward-char (if reverse-p -1 1)))
       (while (and (> depth 0)
-		  (funcall (if reverse-p '> '<) (point) bound))
-	(let ((c (if reverse-p (char-before) (char-after))) nc)
+                  (funcall (if reverse-p #'> #'<) (point) bound))
+        (let ((c (if reverse-p (char-before) (char-after))))
 	  (cond ((and (not reverse-p)
 		      (or (not (eq open close))
 			  backslash-p)
 		      (eq c ?\\)
-		      (setq nc (char-after (1+ (point))))
-		      (or (eq nc open) (eq nc close)))
+                      (memq (char-after (1+ (point)))
+                            (list open close ?\\)))
 		 (forward-char 1))
 		((and reverse-p
 		      (or (not (eq open close))
 			  backslash-p)
-		      (or (eq c open) (eq c close))
-		      (eq (char-before (1- (point)))
-			  ?\\))
+                      (eq (char-before (1- (point))) ?\\)
+                      (memq c (list open close ?\\)))
 		 (forward-char -1))
 		((eq open close)
-		 (if (eq c open)
-		     (if (and (not backslash-p)
-			      (eq (if reverse-p
-				      (char-before (1- (point)))
-				    (char-after (1+ (point)))) open))
-			 (forward-char (if reverse-p -1 1))
-		       (setq depth (1- depth)))))
+                 (when (eq c open)
+                   (if (and (not backslash-p)
+                            (eq (if reverse-p
+                                    (char-before (1- (point)))
+                                  (char-after (1+ (point))))
+                                open))
+                       (forward-char (if reverse-p -1 1))
+                     (setq depth (1- depth)))))
 		((= c open)
 		 (setq depth (+ depth (if reverse-p -1 1))))
 		((= c close)
 		 (setq depth (+ depth (if reverse-p 1 -1))))))
 	(forward-char (if reverse-p -1 1)))
-      (if (= depth 0)
-	  (if reverse-p (point) (1- (point)))))))
+      (when (= depth 0)
+        (if reverse-p (point) (1- (point)))))))
 
 (defun eshell-convert (string)
   "Convert STRING into a more native looking Lisp object."
diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el
index 1e303f70e5..bcc2dc320b 100644
--- a/test/lisp/eshell/eshell-tests.el
+++ b/test/lisp/eshell/eshell-tests.el
@@ -148,9 +148,9 @@ eshell-test/escape-special-quoted
   "Test that the backslash is not preserved for escaped special
 chars"
   (with-temp-eshell
-   (eshell-command-result-p "echo \"h\\\\i\""
+   (eshell-command-result-p "echo \"\\\"hi\\\\\""
                             ;; Backslashes are doubled for regexp.
-                            "h\\\\i\n")))
+                            "\\\"hi\\\\\n")))
 
 (ert-deftest eshell-test/command-running-p ()
   "Modeline should show no command running"
-- 
2.25.1