Subject: bug#27361: Re: bug#27361: 24.4; eshell/echo silently ignores -n argument

From: Jim Porter <[email-masked]>
Date: Sat, 15 Jan 2022 23:19:50 -0800

On 9/15/2020 8:27 AM, Lars Ingebrigtsen wrote:
> Glenn Morris <[email-masked]> writes:
> 
>> -      (concat (apply 'eshell-flatten-and-stringify args) "\n")
>> +      (concat (apply 'eshell-flatten-and-stringify args)
>> +	      (unless output-newline "\n"))
> 
> Hm...  with or without the patch, and with eshell-plain-echo-behavior
> set, I don't see any difference?
> 
> I still get this in eshell:
> 
> ~/src/emacs/trunk $ echo -n foo; echo bar
> foo
> bar

I think this is due to `eshell-lisp-command' insisting that a newline is 
inserted after the output of *every* Lisp command (when executed 
interactively). It's easier to see what "-n" does by eval'ing:

   (eshell-command-result "echo hi")
   ;; and
   (eshell-command-result "echo -n hi")

One solution to this bug would be to set `eshell-ensure-newline-p' to 
nil inside `eshell-echo'; then it will opt out of `eshell-lisp-command's 
trailing-newline insertion.

I've attached a patch that does this, as well as (hopefully) resolving 
the unusual meaning of "-n" when echo outputs a Lisp-friendly result 
(i.e. when `eshell-plain-echo-behavior' is nil). Now, it warns the user 
to use "-N" instead if they want to add a trailing newline. Then, one 
day, the old meaning of "-n" can be removed. I'm not sure if this is the 
best way to emit a warning about this though; I wanted something that 
doesn't interfere with any Eshell scripts, but `display-warning' is a 
bit in-your-face.

I also simplified the logic of `eshell/printnl'. Since the way it called 
`eshell-echo' always resulted in it printing the stringified form of 
each argument, it's simpler just to call `eshell-stringify' directly.

Finally, I added a bit more detail about echo to the Eshell manual 
(taken partly from the commentary of eshell/em-basic.el), since I didn't 
quite understand its behavior initially.
From 45d10c678a3c58486e0571d3c3e8ce64feb39299 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Sat, 15 Jan 2022 15:16:31 -0800
Subject: [PATCH] In Eshell, allow "-n" to suppress the trailing newline for
 "plain" echo

In addition, add a "-N" option to explicitly request a newline.

* lisp/eshell/em-basic.el (eshell-echo): Respect OUTPUT-NEWLINE even
when 'eshell-plain-echo-behavior' is non-nil.
(eshell/echo): Add "-N" option and recommend its use over "-n" in
Lisp-friendly echo.
(eshell/printnl): Simplify; 'eshell-stringify' is equivalent to
calling 'eshell-echo' here.

* doc/misc/eshell.texi (Built-in commands): Expand on the
documentation of echo.
---
 doc/misc/eshell.texi    | 13 ++++++++++---
 lisp/eshell/em-basic.el | 30 ++++++++++++++++++++++--------
 2 files changed, 32 insertions(+), 11 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index f1d7c63805..df6e3b861e 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -407,9 +407,16 @@ Built-ins
 
 @item echo
 @cmindex echo
-Echoes its input.  If @code{eshell-plain-echo-behavior} is
-non-@code{nil}, @command{echo} will try to behave more like a plain
-shell's @command{echo}.
+Echoes its input.  By default, this prints in a Lisp-friendly fashion
+(so that the value is useful to a Lisp command using the result of
+@command{echo} as an argument).  If a single argument is passed,
+@command{echo} prints that; if multiple arguments are passed, it
+prints a list of all the arguments; otherwise, it prints the empty
+string.
+
+If @code{eshell-plain-echo-behavior} is non-@code{nil}, @command{echo}
+will try to behave more like a plain shell's @command{echo}, printing
+each argument as a string, separated by a space.
 
 @item env
 @cmindex env
diff --git a/lisp/eshell/em-basic.el b/lisp/eshell/em-basic.el
index 27b343ad39..d3b15c900b 100644
--- a/lisp/eshell/em-basic.el
+++ b/lisp/eshell/em-basic.el
@@ -82,7 +82,11 @@ eshell-echo
 It returns a formatted value that should be passed to `eshell-print'
 or `eshell-printn' for display."
   (if eshell-plain-echo-behavior
-      (concat (apply 'eshell-flatten-and-stringify args) "\n")
+      (progn
+        ;; If the output does not end in a newline, do not emit one.
+        (setq eshell-ensure-newline-p nil)
+        (concat (apply #'eshell-flatten-and-stringify args)
+                (when output-newline "\n")))
     (let ((value
 	   (cond
 	    ((= (length args) 0) "")
@@ -109,18 +113,28 @@ eshell/echo
   "Implementation of `echo'.  See `eshell-plain-echo-behavior'."
   (eshell-eval-using-options
    "echo" args
-   '((?n nil nil output-newline "terminate with a newline")
+   '((?n nil (nil) output-newline "do not output the trailing newline")
+     (?N nil (t)   output-newline "terminate with a newline")
      (?h "help" nil nil "output this help screen")
      :preserve-args
-     :usage "[-n] [object]")
-   (eshell-echo args output-newline)))
+     :usage "[-n | -N] [object]")
+   (if eshell-plain-echo-behavior
+       (eshell-echo args (if output-newline (car output-newline) t))
+     ;; In Emacs 28.1 and earlier, "-n" was used to add a newline to
+     ;; non-plain echo in Eshell.  This caused confusion due to "-n"
+     ;; generally having the opposite meaning for echo.  Retain this
+     ;; compatibility for the time being.  For more info, see
+     ;; bug#27361.
+     (when (equal output-newline '(nil))
+       (display-warning
+        :warning "To terminate with a newline, you should use -N instead."))
+     (eshell-echo args output-newline))))
 
 (defun eshell/printnl (&rest args)
-  "Print out each of the arguments, separated by newlines."
+  "Print out each of the arguments as strings, separated by newlines."
   (let ((elems (flatten-tree args)))
-    (while elems
-      (eshell-printn (eshell-echo (list (car elems))))
-      (setq elems (cdr elems)))))
+    (dolist (elem elems)
+      (eshell-printn (eshell-stringify elem)))))
 
 (defun eshell/listify (&rest args)
   "Return the argument(s) as a single list."
-- 
2.25.1