Subject: bug#65604: [PATCH] Display the exit code if the last command failed in Eshell

From: Davide Masserut <[email-masked]>
Date: Wed, 30 Aug 2023 22:20:56 +0200

I updated the NEWS file, the manual and used the ":eval" construct 
to update "mode-line-process".

>From 33e97ff350e1184f97c2867bd689a77291a2c7fd Mon Sep 17 00:00:00 2001
From: Davide Masserut <[email-masked]>
Date: Wed, 30 Aug 2023 21:34:08 +0200
Subject: [PATCH] Display the exit code if the last command failed in Eshell

* doc/misc/eshell.texi (Invocation): Document change.
* etc/NEWS: Announce change.
* lisp/eshell/esh-io.el (eshell-last-command-status): Make it buffer-local.
* lisp/eshell/esh-mode.el (compile): Require it for a face.
(eshell-mode): Update mode-line-process based on the exit code of the
last command.
* test/lisp/eshell/esh-io-tests.el (eshell)
(esh-io-test/modeline-after-failure): Add test.
---
 doc/misc/eshell.texi             |  3 +++
 etc/NEWS                         |  3 +++
 lisp/eshell/esh-io.el            |  2 +-
 lisp/eshell/esh-mode.el          | 12 ++++++++++++
 test/lisp/eshell/esh-io-tests.el | 16 ++++++++++++++++
 5 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index f8f60bae13a..b544e0cda41 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -234,6 +234,9 @@ Invocation
 can be controlled the same way as any other background process in
 Emacs.
 
+If a command exits abnormally, Eshell displays the command's exit code
+on the mode line.
+
 @subsection Command form
 Command form looks much the same as in other shells.  A command
 consists of arguments separated by spaces; the first argument is the
diff --git a/etc/NEWS b/etc/NEWS
index 9a98db8c83a..fe81cfc2477 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -370,6 +370,9 @@ to load the edited aliases.
 Running 'rgrep' in Eshell now uses the Emacs grep facility instead of
 calling external rgrep.
 
++++
+*** If a command exits abnormally, Eshell now displays the command's exit code on the mode line.
+
 ** Pcomplete
 
 ---
diff --git a/lisp/eshell/esh-io.el b/lisp/eshell/esh-io.el
index c07f871dd37..cd0cee6e21d 100644
--- a/lisp/eshell/esh-io.el
+++ b/lisp/eshell/esh-io.el
@@ -170,7 +170,7 @@ eshell-redirection-operators-alist
 
 (defvar eshell-current-handles nil)
 
-(defvar eshell-last-command-status 0
+(defvar-local eshell-last-command-status 0
   "The exit code from the last command.  0 if successful.")
 
 (defvar eshell-last-command-result nil
diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 0c381dbb86a..71ff901c764 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -69,6 +69,8 @@
 (require 'esh-util)
 (require 'esh-var)
 
+(require 'compile)
+
 (defgroup eshell-mode nil
   "This module contains code for handling input from the user."
   :tag "User interface"
@@ -369,6 +371,16 @@ eshell-mode
   ;; strong R2L character.
   (setq bidi-paragraph-direction 'left-to-right)
 
+  (setq-local
+   mode-line-process
+   '(:eval
+     (when (> eshell-last-command-status 0)
+       (propertize
+        (format ":[%s]" eshell-last-command-status)
+        'help-echo (format "Last command exited with code %s"
+                           eshell-last-command-status)
+        'face 'compilation-mode-line-fail))))
+
   ;; load extension modules into memory.  This will cause any global
   ;; variables they define to be visible, since some of the core
   ;; modules sometimes take advantage of their functionality if used.
diff --git a/test/lisp/eshell/esh-io-tests.el b/test/lisp/eshell/esh-io-tests.el
index ce80f3a8f08..349c4332d82 100644
--- a/test/lisp/eshell/esh-io-tests.el
+++ b/test/lisp/eshell/esh-io-tests.el
@@ -23,6 +23,7 @@
 (require 'ert-x)
 (require 'esh-mode)
 (require 'eshell)
+(require 'compile)
 
 (require 'eshell-tests-helpers
          (expand-file-name "eshell-tests-helpers"
@@ -370,4 +371,19 @@ esh-io-test/virtual/dev-kill
    (eshell-insert-command "echo three >> /dev/kill")
    (should (equal (car kill-ring) "twothree"))))
 
+(ert-deftest esh-io-test/modeline-after-failure ()
+  "Check that exit code is displayed after a failure."
+  (with-temp-eshell
+   (let ((debug-on-error nil))
+     (eshell-insert-command "(zerop \"foo\")")) ; A failed command.
+   (should (equal-including-properties
+            mode-line-process
+            '(:eval
+              (when (> eshell-last-command-status 0)
+                (propertize
+                 (format ":[%s]" eshell-last-command-status)
+                 'help-echo (format "Last command exited with code %s"
+                                    eshell-last-command-status)
+                 'face 'compilation-mode-line-fail)))))))
+
 ;;; esh-io-tests.el ends here
-- 
2.42.0