Subject: Re: bug#70847: 29.3; eshell scripts "from anywhere"

From: Jim Porter <[email-masked]>
Date: Thu, 23 May 2024 13:30:09 -0700

On 5/9/2024 10:57 AM, Christopher Howard wrote:
> ``` ~/Scratch/script.esh
> echo hello world
> ```
> 
> M-: (eshell-source-file "~/Scratch/script.esh")
> 
> gives
> 
> eval-expression: No catch for tag: eshell-replace-command, (let ((eshell-command-name '"~/Scratch/script.esh") (eshell-command-arguments 'nil)) (eshell-trap-errors (eshell-named-command "echo" (list "hello" "world"))))

Looking at this code, I doubt 'eshell-source-file' ever worked, so the 
documentation is probably just wrong.

Do the attached patches help you? They also add support for running 
Eshell scripts from the command line using a shebang.

From 201f8b1329d88985d29ca682b070b21ce7462f1e Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Fri, 10 Mar 2023 19:02:26 -0800
Subject: [PATCH 1/4] Consolidate Eshell module loading/unloading code

This also adds the ability to suppress module loading/unloading
messages, which will be necessary to support running Eshell scripts as
batch scripts.

* lisp/eshell/esh-mode.el (eshell-mode): Move module
loading/initialization to...

* lisp/eshell/esh-module.el (eshell-load-modules)
(eshell-initialize-modules): ... here.
(eshell-module-loading-messages): New option.
(eshell-module--feature-name): Improve docstring.
(eshell-unload-modules): Display a real warning if unable to unload a
module.

* test/lisp/eshell/eshell-tests-helpers.el (with-temp-eshell)
(eshell-command-result-equal):
* test/lisp/eshell/eshell-tests-unload.el (load-eshell): Silence Eshell
loading messages.
---
 lisp/eshell/esh-mode.el                  | 31 ++---------
 lisp/eshell/esh-module.el                | 70 ++++++++++++++++++++----
 test/lisp/eshell/eshell-tests-helpers.el | 10 ++--
 test/lisp/eshell/eshell-tests-unload.el  |  2 +
 4 files changed, 72 insertions(+), 41 deletions(-)

diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 78a448a41a5..31a9216be33 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -372,36 +372,15 @@ eshell-mode
   ;; strong R2L character.
   (setq bidi-paragraph-direction 'left-to-right)
 
-  ;; 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.
-  (dolist (module eshell-modules-list)
-    (let ((module-fullname (symbol-name module))
-	  module-shortname)
-      (if (string-match "^eshell-\\(.*\\)" module-fullname)
-	  (setq module-shortname
-		(concat "em-" (match-string 1 module-fullname))))
-      (unless module-shortname
-	(error "Invalid Eshell module name: %s" module-fullname))
-      (unless (featurep (intern module-shortname))
-        (condition-case nil
-            (load module-shortname)
-          (error (lwarn 'eshell :error
-                        "Unable to load module `%s' (defined in `eshell-modules-list')"
-                        module-fullname))))))
+  ;; Load extension modules into memory.
+  (eshell-load-modules eshell-modules-list)
 
   (unless (file-exists-p eshell-directory-name)
     (eshell-make-private-directory eshell-directory-name t))
 
-  ;; Load core Eshell modules, then extension modules, for this session.
-  (dolist (module (append (eshell-subgroups 'eshell) eshell-modules-list))
-    (let ((load-hook (intern-soft (format "%s-load-hook" module)))
-          (initfunc (intern-soft (format "%s-initialize" module))))
-      (when (and load-hook (boundp load-hook))
-        (if (memq initfunc (symbol-value load-hook)) (setq initfunc nil))
-        (run-hooks load-hook))
-      ;; So we don't need the -initialize functions on the hooks (bug#5375).
-      (and initfunc (fboundp initfunc) (funcall initfunc))))
+  ;; Initialize core Eshell modules, then extension modules, for this session.
+  (eshell-initialize-modules (eshell-subgroups 'eshell))
+  (eshell-initialize-modules eshell-modules-list)
 
   (if eshell-send-direct-to-subprocesses
       (add-hook 'pre-command-hook #'eshell-intercept-commands t t))
diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el
index fbd5ae4b9b8..88221638e16 100644
--- a/lisp/eshell/esh-module.el
+++ b/lisp/eshell/esh-module.el
@@ -49,6 +49,12 @@ eshell-module-unload-hook
   :group 'eshell-module)
 (make-obsolete-variable 'eshell-module-unload-hook nil "30.1")
 
+(defcustom eshell-module-loading-messages t
+  "If non-nil, display messages when loading/unloading Eshell modules."
+  :type 'boolean
+  :group 'eshell-module
+  :version "30.1")
+
 (defcustom eshell-modules-list
   '(eshell-alias
     eshell-banner
@@ -87,7 +93,9 @@ eshell-modules-list
 ;;; Code:
 
 (defsubst eshell-module--feature-name (module &optional kind)
-  "Get the feature name for the specified Eshell MODULE."
+  "Get the feature name for the specified Eshell MODULE.
+KIND can be either `core' for a core module or `extension' for an
+extension module; if nil, KIND defaults to `extension'."
   (let ((module-name (symbol-name module))
         (prefix (cond ((eq kind 'core) "esh-")
                       ((memq kind '(extension nil)) "em-")
@@ -102,17 +110,57 @@ eshell-using-module
 customization group.  Example: `eshell-cmpl' for that module."
   (memq module eshell-modules-list))
 
-(defun eshell-unload-modules (modules &optional kind)
-  "Try to unload the specified Eshell MODULES."
+(defun eshell-load-modules (modules)
+  "Load Eshell MODULES into memory.
+This will cause any global variables they define to be visible so
+that other modules can take advantage of their functionality if
+desired."
+  (let ((verbose eshell-module-loading-messages))
+    (dolist (module modules)
+      (let ((module-feature-name (eshell-module--feature-name module)))
+        (unless (featurep (intern module-feature-name))
+          (when verbose (message "Loading %s..." module))
+          (condition-case-unless-debug nil
+              (progn
+                (load module-feature-name nil t)
+                (when verbose (message "Loading %s...done" module)))
+            (error (when verbose (message "Loading %s...failed" module))
+                   (lwarn 'eshell :error
+                          "Unable to load Eshell module `%s'"
+                          module))))))))
+
+(defun eshell-initialize-modules (modules)
+  "Initialize Eshell MODULES.
+This calls `MODULE-load-hook' and `MODULE-initialize' for each
+MODULE, if they're defined."
   (dolist (module modules)
-    (let ((module-feature (intern (eshell-module--feature-name module kind))))
-      (when (featurep module-feature)
-	(message "Unloading %s..." (symbol-name module))
-        (condition-case-unless-debug _
-            (progn
-              (unload-feature module-feature)
-              (message "Unloading %s...done" (symbol-name module)))
-          (error (message "Unloading %s...failed" (symbol-name module))))))))
+    (let ((load-hook (intern-soft (format "%s-load-hook" module)))
+          (initfunc (intern-soft (format "%s-initialize" module))))
+      (when (and load-hook (boundp load-hook))
+        (if (memq initfunc (symbol-value load-hook)) (setq initfunc nil))
+        (run-hooks load-hook))
+      ;; So we don't need the -initialize functions on the hooks (bug#5375).
+      (and initfunc (fboundp initfunc) (funcall initfunc)))))
+
+(defun eshell-unload-modules (modules &optional kind)
+  "Try to unload the specified Eshell MODULES.
+KIND can be either `core' for core modules or `extension' for
+extension modules; if nil, KIND defaults to `extension'."
+  ;; We're about to unload this module, but we need to remember whether
+  ;; to print messages.
+  (let ((verbose eshell-module-loading-messages))
+    (dolist (module modules)
+      (let ((module-feature (intern (eshell-module--feature-name module kind))))
+        (when (featurep module-feature)
+          (when verbose (message "Unloading %s..." module))
+          (condition-case-unless-debug nil
+              (progn
+                (unload-feature module-feature)
+                (when verbose (message "Unloading %s...done" module)))
+            (error (when verbose (message "Unloading %s...failed" module))
+                   (lwarn 'eshell :error
+                          "Unable to unload Eshell module `%s'"
+                          module))))))))
 
 (defun eshell-unload-extension-modules ()
   "Try to unload all currently-loaded Eshell extension modules."
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index 652146fefcc..3f1c55f420d 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -63,6 +63,7 @@ with-temp-eshell
               (eshell-debug-command (cons 'process eshell-debug-command))
               (eshell-history-file-name nil)
               (eshell-last-dir-ring-file-name nil)
+              (eshell-module-loading-messages nil)
               (eshell-buffer (eshell t)))
          (unwind-protect
              (with-current-buffer eshell-buffer
@@ -183,10 +184,11 @@ eshell-command-result--equal-explainer
 (defun eshell-command-result-equal (command result)
   "Execute COMMAND non-interactively and compare it to RESULT."
   (ert-info (#'eshell-get-debug-logs :prefix "Command logs: ")
-    (should (eshell-command-result--equal
-             command
-             (eshell-test-command-result command)
-             result))))
+    (let ((eshell-module-loading-messages nil))
+      (should (eshell-command-result--equal
+               command
+               (eshell-test-command-result command)
+               result)))))
 
 (provide 'eshell-tests-helpers)
 
diff --git a/test/lisp/eshell/eshell-tests-unload.el b/test/lisp/eshell/eshell-tests-unload.el
index bf8291ba47a..479090e8f8c 100644
--- a/test/lisp/eshell/eshell-tests-unload.el
+++ b/test/lisp/eshell/eshell-tests-unload.el
@@ -33,6 +33,7 @@ eshell-directory-name
 (defvar eshell-history-file-name)
 (defvar eshell-last-dir-ring-file-name)
 (defvar eshell-modules-list)
+(defvar eshell-module-loading-messages)
 
 (declare-function eshell-module--feature-name "esh-module"
                   (module &optional kind))
@@ -51,6 +52,7 @@ load-eshell
              (process-environment (cons "HISTFILE" process-environment))
              (eshell-history-file-name nil)
              (eshell-last-dir-ring-file-name nil)
+             (eshell-module-loading-messages nil)
              (eshell-buffer (eshell t)))
         (let (kill-buffer-query-functions)
           (kill-buffer eshell-buffer))))))
-- 
2.25.1

From bc1828265c226fa3b5267eca2c3d3f08bedfe627 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Sun, 19 May 2024 22:01:31 -0700
Subject: [PATCH 2/4] Use 'esh-module-autoload' for Eshell modules' defgroups

This will let modules define their own, regular autoloads, independent
of the core Eshell machinery for defining modules.

* lisp/eshell/em-alias.el (em-alias):
* lisp/eshell/em-banner.el (em-banner):
* lisp/eshell/em-basic.el (em-basic):
* lisp/eshell/em-cmpl.el (em-cmpl):
* lisp/eshell/em-dirs.el (em-dirs):
* lisp/eshell/em-elecslash.el (em-elecslash):
* lisp/eshell/em-extpipe.el:
* lisp/eshell/em-glob.el (em-glob):
* lisp/eshell/em-hist.el (em-hist):
* lisp/eshell/em-ls.el (em-ls):
* lisp/eshell/em-pred.el (em-pred):
* lisp/eshell/em-prompt.el (em-prompt):
* lisp/eshell/em-rebind.el (em-rebind):
* lisp/eshell/em-script.el (em-script):
* lisp/eshell/em-smart.el (em-smart):
* lisp/eshell/em-term.el (em-term):
* lisp/eshell/em-tramp.el (em-tramp):
* lisp/eshell/em-unix.el (em-unix):
* lisp/eshell/em-xtra.el (em-xtra): Use 'esh-module-autoload'.

* lisp/eshell/esh-module.el ("esh-module-loaddefs"): Load this instead
of "esh-groups".

* .gitignore: Change esh-groups.el to esh-module-loaddefs.el
---
 .gitignore                  | 2 +-
 lisp/eshell/em-alias.el     | 7 +------
 lisp/eshell/em-banner.el    | 7 +------
 lisp/eshell/em-basic.el     | 7 +------
 lisp/eshell/em-cmpl.el      | 7 +------
 lisp/eshell/em-dirs.el      | 7 +------
 lisp/eshell/em-elecslash.el | 7 +------
 lisp/eshell/em-extpipe.el   | 2 +-
 lisp/eshell/em-glob.el      | 7 +------
 lisp/eshell/em-hist.el      | 7 +------
 lisp/eshell/em-ls.el        | 7 +------
 lisp/eshell/em-pred.el      | 7 +------
 lisp/eshell/em-prompt.el    | 7 +------
 lisp/eshell/em-rebind.el    | 7 +------
 lisp/eshell/em-script.el    | 7 +------
 lisp/eshell/em-smart.el     | 7 +------
 lisp/eshell/em-term.el      | 7 +------
 lisp/eshell/em-tramp.el     | 7 +------
 lisp/eshell/em-unix.el      | 7 +------
 lisp/eshell/em-xtra.el      | 7 +------
 lisp/eshell/esh-module.el   | 2 +-
 21 files changed, 21 insertions(+), 111 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1557c085fad..52d328a9357 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,7 +128,7 @@ lisp/cedet/semantic/wisent/js-wy.el
 lisp/cedet/semantic/wisent/python-wy.el
 lisp/cedet/srecode/srt-wy.el
 lisp/cedet/semantic/grammar-wy.el
-lisp/eshell/esh-groups.el
+lisp/eshell/esh-module-loaddefs.el
 lisp/finder-inf.el
 lisp/leim/ja-dic/
 leim/small-ja-dic-option
diff --git a/lisp/eshell/em-alias.el b/lisp/eshell/em-alias.el
index 832e14418d0..d12b382d885 100644
--- a/lisp/eshell/em-alias.el
+++ b/lisp/eshell/em-alias.el
@@ -92,7 +92,7 @@
 
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-alias nil
   "Command aliases allow for easy definition of alternate commands."
@@ -268,9 +268,4 @@ eshell-fix-bad-commands
 		     (eshell-parse-command alias))))))))))
 
 (provide 'em-alias)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-alias.el ends here
diff --git a/lisp/eshell/em-banner.el b/lisp/eshell/em-banner.el
index e6dcbb24475..626624c7bfe 100644
--- a/lisp/eshell/em-banner.el
+++ b/lisp/eshell/em-banner.el
@@ -44,7 +44,7 @@
 (require 'esh-util)
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-banner nil
   "This sample module displays a welcome banner at login.
@@ -82,9 +82,4 @@ eshell-banner-initialize
       (eshell-interactive-print msg))))
 
 (provide 'em-banner)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-banner.el ends here
diff --git a/lisp/eshell/em-basic.el b/lisp/eshell/em-basic.el
index 6ec53ef9412..82cddd7385f 100644
--- a/lisp/eshell/em-basic.el
+++ b/lisp/eshell/em-basic.el
@@ -58,7 +58,7 @@
 (require 'esh-opt)
 (require 'esh-util)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-basic nil
   "The \"basic\" code provides a set of convenience functions which
@@ -225,9 +225,4 @@ pcomplete/eshell-mode/eshell-debug
   (while (pcomplete-here '("error" "form" "process"))))
 
 (provide 'em-basic)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-basic.el ends here
diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el
index 201beb5071d..4c79f7b187a 100644
--- a/lisp/eshell/em-cmpl.el
+++ b/lisp/eshell/em-cmpl.el
@@ -76,7 +76,7 @@
 
 (eval-when-compile (require 'cl-lib))
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-cmpl nil
   "This module provides a programmable completion function bound to
@@ -518,9 +518,4 @@ eshell--complete-commands-list
 (define-obsolete-function-alias 'eshell-pcomplete #'completion-at-point "27.1")
 
 (provide 'em-cmpl)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-cmpl.el ends here
diff --git a/lisp/eshell/em-dirs.el b/lisp/eshell/em-dirs.el
index 07063afc286..a3d1a349540 100644
--- a/lisp/eshell/em-dirs.el
+++ b/lisp/eshell/em-dirs.el
@@ -47,7 +47,7 @@
 (require 'ring)
 (require 'esh-opt)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-dirs nil
   "Directory navigation involves changing directories, examining the
@@ -599,9 +599,4 @@ eshell-write-last-dir-ring
 			 'no-message))))))))
 
 (provide 'em-dirs)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-dirs.el ends here
diff --git a/lisp/eshell/em-elecslash.el b/lisp/eshell/em-elecslash.el
index 93eadcfe1ff..60f2c6e4039 100644
--- a/lisp/eshell/em-elecslash.el
+++ b/lisp/eshell/em-elecslash.el
@@ -32,7 +32,7 @@
 (require 'esh-mode)
 
 ;; This makes us an option when customizing `eshell-modules-list'.
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-elecslash nil
   "Electric forward slash in remote Eshells.
@@ -108,9 +108,4 @@ eshell-electric-forward-slash
         (insert "/")))))
 
 (provide 'em-elecslash)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; esh-elecslash.el ends here
diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el
index 057eead9297..4c92653a3a3 100644
--- a/lisp/eshell/em-extpipe.el
+++ b/lisp/eshell/em-extpipe.el
@@ -36,7 +36,7 @@
 
 (eval-when-compile (require 'files-x))
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-extpipe nil
   "Native shell pipelines.
diff --git a/lisp/eshell/em-glob.el b/lisp/eshell/em-glob.el
index 89a40151d00..36e4f90aed2 100644
--- a/lisp/eshell/em-glob.el
+++ b/lisp/eshell/em-glob.el
@@ -53,7 +53,7 @@
 (require 'esh-module)
 (require 'esh-util)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-glob nil
   "This module provides extended globbing syntax, similar what is used
@@ -417,9 +417,4 @@ eshell-glob-entries
         (eshell-glob-entries rdir globs only-dirs)))))
 
 (provide 'em-glob)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-glob.el ends here
diff --git a/lisp/eshell/em-hist.el b/lisp/eshell/em-hist.el
index 21029eae1bc..22d7723943a 100644
--- a/lisp/eshell/em-hist.el
+++ b/lisp/eshell/em-hist.el
@@ -60,7 +60,7 @@
 (require 'esh-opt)
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-hist nil
   "This module provides command history management."
@@ -1068,9 +1068,4 @@ em-hist-unload-function
   (remove-hook 'kill-emacs-hook 'eshell-save-some-history))
 
 (provide 'em-hist)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-hist.el ends here
diff --git a/lisp/eshell/em-ls.el b/lisp/eshell/em-ls.el
index 62ad7ff72a1..82d4b01393f 100644
--- a/lisp/eshell/em-ls.el
+++ b/lisp/eshell/em-ls.el
@@ -32,7 +32,7 @@
 (require 'esh-proc)
 (require 'esh-cmd)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-ls nil
   "This module implements the \"ls\" utility fully in Lisp.
@@ -956,9 +956,4 @@ em-ls-unload-function
   (eshell-ls-disable-in-dired))
 
 (provide 'em-ls)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-ls.el ends here
diff --git a/lisp/eshell/em-pred.el b/lisp/eshell/em-pred.el
index c3997dc72c3..0be262641ea 100644
--- a/lisp/eshell/em-pred.el
+++ b/lisp/eshell/em-pred.el
@@ -48,7 +48,7 @@
 
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-pred nil
   "This module allows for predicates to be applied to globbing
@@ -576,9 +576,4 @@ eshell-split-members
        lst))))
 
 (provide 'em-pred)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-pred.el ends here
diff --git a/lisp/eshell/em-prompt.el b/lisp/eshell/em-prompt.el
index 3662c1fa895..b6556d29544 100644
--- a/lisp/eshell/em-prompt.el
+++ b/lisp/eshell/em-prompt.el
@@ -29,7 +29,7 @@
 (require 'esh-mode)
 (require 'text-property-search)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-prompt nil
   "This module provides command prompts, and navigation between them,
@@ -231,9 +231,4 @@ eshell-bol-ignoring-prompt
     (move-beginning-of-line arg)))
 
 (provide 'em-prompt)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-prompt.el ends here
diff --git a/lisp/eshell/em-rebind.el b/lisp/eshell/em-rebind.el
index c6ee1a329b6..aad65f66f41 100644
--- a/lisp/eshell/em-rebind.el
+++ b/lisp/eshell/em-rebind.el
@@ -25,7 +25,7 @@
 
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-rebind nil
   "This module allows for special keybindings that only take effect
@@ -250,9 +250,4 @@ eshell-delchar-or-maybe-eof
       (eshell-delete-backward-char (- arg)))))
 
 (provide 'em-rebind)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-rebind.el ends here
diff --git a/lisp/eshell/em-script.el b/lisp/eshell/em-script.el
index 066063c4cc2..254a11ea114 100644
--- a/lisp/eshell/em-script.el
+++ b/lisp/eshell/em-script.el
@@ -25,7 +25,7 @@
 
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-script nil
   "This module allows for the execution of files containing Eshell
@@ -115,9 +115,4 @@ eshell/.
 (put 'eshell/. 'eshell-no-numeric-conversions t)
 
 (provide 'em-script)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-script.el ends here
diff --git a/lisp/eshell/em-smart.el b/lisp/eshell/em-smart.el
index 91fe02e5545..670b956476d 100644
--- a/lisp/eshell/em-smart.el
+++ b/lisp/eshell/em-smart.el
@@ -70,7 +70,7 @@
 
 (require 'esh-mode)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-smart nil
   "This module combines the facility of normal, modern shells with
@@ -303,9 +303,4 @@ em-smart-unload-hook
   (remove-hook 'window-configuration-change-hook #'eshell-smart-scroll))
 
 (provide 'em-smart)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-smart.el ends here
diff --git a/lisp/eshell/em-term.el b/lisp/eshell/em-term.el
index 8f29e2d8509..2f8c06a0baa 100644
--- a/lisp/eshell/em-term.el
+++ b/lisp/eshell/em-term.el
@@ -36,7 +36,7 @@
 (require 'esh-ext)
 (require 'term)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-term nil
   "This module causes visual commands (e.g., `vi') to be executed by
@@ -330,9 +330,4 @@ eshell-term-sentinel
 ;  (use-local-map term-old-mode-map))
 
 (provide 'em-term)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-term.el ends here
diff --git a/lisp/eshell/em-tramp.el b/lisp/eshell/em-tramp.el
index efb37225651..cfc8a47327e 100644
--- a/lisp/eshell/em-tramp.el
+++ b/lisp/eshell/em-tramp.el
@@ -35,7 +35,7 @@
 
 ;; There are no items in this custom group, but eshell modules (ab)use
 ;; custom groups.
-;;;###autoload
+;;;###esh-module-autoload
 (progn
  (defgroup eshell-tramp nil
    "This module defines commands that use Tramp in a way that is
@@ -152,9 +152,4 @@ eshell/doas
 (put 'eshell/doas 'eshell-no-numeric-conversions t)
 
 (provide 'em-tramp)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-tramp.el ends here
diff --git a/lisp/eshell/em-unix.el b/lisp/eshell/em-unix.el
index 855efa26033..7f976d22681 100644
--- a/lisp/eshell/em-unix.el
+++ b/lisp/eshell/em-unix.el
@@ -38,7 +38,7 @@
 (require 'esh-mode)
 (require 'pcomplete)
 
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-unix nil
   "This module defines many of the more common UNIX utilities as
@@ -1096,9 +1096,4 @@ eshell-diff-window-config
 (define-obsolete-function-alias 'eshell-diff-quit #'ignore "30.1")
 
 (provide 'em-unix)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-unix.el ends here
diff --git a/lisp/eshell/em-xtra.el b/lisp/eshell/em-xtra.el
index a62073e2183..0a032395fd3 100644
--- a/lisp/eshell/em-xtra.el
+++ b/lisp/eshell/em-xtra.el
@@ -28,7 +28,7 @@
 
 ;; There are no items in this custom group, but eshell modules (ab)use
 ;; custom groups.
-;;;###autoload
+;;;###esh-module-autoload
 (progn
 (defgroup eshell-xtra nil
   "This module defines some extra alias functions which are entirely
@@ -85,9 +85,4 @@ 'eshell/ff
 (defalias 'eshell/gf #'find-grep-dired)
 
 (provide 'em-xtra)
-
-;; Local Variables:
-;; generated-autoload-file: "esh-groups.el"
-;; End:
-
 ;;; em-xtra.el ends here
diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el
index 88221638e16..db808f8597a 100644
--- a/lisp/eshell/esh-module.el
+++ b/lisp/eshell/esh-module.el
@@ -38,7 +38,7 @@ eshell-module
 ;; `eshell-modules-list'.  We use "(progn (defgroup ..." in each file
 ;; to force the autoloader into including the entire defgroup, rather
 ;; than an abbreviated version.
-(load "esh-groups" nil 'nomessage)
+(load "esh-module-loaddefs" nil 'nomessage)
 
 ;;; User Variables:
 
-- 
2.25.1

From 7d385585f48c9c7093ca1a4ff0448028f9fade46 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Wed, 22 May 2024 21:58:34 -0700
Subject: [PATCH 3/4] Rework how 'eshell-ensure-newline-p' adds newlines

This allows for other output targets (see the following commit) to be
"line-oriented".

* lisp/eshell/esh-io.el (eshell-virtual-targets): Update docstring.
(eshell-ensure-newline-p): Move from esh-cmd.el.
(eshell-generic-target): New struct...
(eshell-function-target): ... use it, and rename from
'eshell-virtual-target'.
(eshell-target-line-oriented-p, eshell--output-maybe-n)
(eshell-print-maybe-n, eshell-error-maybe-n)
(eshell-maybe-output-newline): New functions.

* lisp/eshell/esh-cmd.el (eshell-lisp-command): Don't print a newline
in this function directly; instead, use 'eshell-print-maybe-n' and
'eshell-error-maybe-n'.
(eshell-ensure-newline-p): Move to esh-io.el.

* lisp/eshell/em-unix.el (eshell/cat): Remove now-unnecessary override
of 'eshell-ensure-newline-p'.

* doc/misc/eshell.texi (Redirection): Correct documentation about
virtual targets.
---
 doc/misc/eshell.texi   |   4 +-
 lisp/eshell/em-unix.el |   4 +-
 lisp/eshell/esh-cmd.el |  20 +++------
 lisp/eshell/esh-io.el  | 100 ++++++++++++++++++++++++++++++++---------
 4 files changed, 89 insertions(+), 39 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 57ee3bf3e9f..bdf19bb6bd9 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -2402,8 +2402,8 @@ Redirection
 @code{append} for @code{>>}, or @code{insert} for @code{>>>}--and the
 function is expected to return the output function.
 
-The output function is called once on each line of output until
-@code{nil} is passed, indicating end of output.
+When using the virtual target, Eshell will call the output function with
+each object to output in turn.
 
 @node Pipelines
 @section Pipelines
diff --git a/lisp/eshell/em-unix.el b/lisp/eshell/em-unix.el
index 7f976d22681..4137c05fa41 100644
--- a/lisp/eshell/em-unix.el
+++ b/lisp/eshell/em-unix.el
@@ -683,9 +683,7 @@ eshell/cat
 	       (with-current-buffer curbuf
 		 (eshell-buffered-print str)))
 	     (forward-line)))))
-     (eshell-flush)
-     ;; if the file does not end in a newline, do not emit one
-     (setq eshell-ensure-newline-p nil))))
+     (eshell-flush))))
 
 (put 'eshell/cat 'eshell-no-numeric-conversions t)
 (put 'eshell/cat 'eshell-filename-arguments t)
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index dae1a77552f..f77f910e63f 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -254,11 +254,6 @@ eshell-subcommand-bindings
   :type 'sexp
   :risky t)
 
-(defvar eshell-ensure-newline-p nil
-  "If non-nil, ensure that a newline is emitted after a Lisp form.
-This can be changed by Lisp forms that are evaluated from the Eshell
-command line.")
-
 ;;; Internal Variables:
 
 ;; These variables have been merged into `eshell-foreground-command'.
@@ -1497,7 +1492,7 @@ eshell-lisp-command
   (catch 'eshell-external               ; deferred to an external command
     (setq eshell-last-command-status 0
           eshell-last-arguments args)
-    (let* ((eshell-ensure-newline-p (eshell-interactive-output-p))
+    (let* ((eshell-ensure-newline-p t)
            (command-form-p (functionp object))
            (result
             (if command-form-p
@@ -1524,14 +1519,13 @@ eshell-lisp-command
                       (setq args (cdr args))))
                   (setq eshell-last-command-name
                         (concat "#<function " (symbol-name object) ">"))
-                  (eshell-apply object eshell-last-arguments))
+                  (eshell-apply* #'eshell-print-maybe-n
+                                 #'eshell-error-maybe-n
+                                 object eshell-last-arguments))
               (setq eshell-last-command-name "#<Lisp object>")
-              (eshell-eval object))))
-      (if (and eshell-ensure-newline-p
-	       (save-excursion
-		 (goto-char eshell-last-output-end)
-		 (not (bolp))))
-	  (eshell-print "\n"))
+              (eshell-eval* #'eshell-print-maybe-n
+                            #'eshell-error-maybe-n
+                            object))))
       (eshell-close-handles
        ;; If `eshell-lisp-form-nil-is-failure' is non-nil, Lisp forms
        ;; that succeeded but have a nil result should have an exit
diff --git a/lisp/eshell/esh-io.el b/lisp/eshell/esh-io.el
index 4487389bf26..83b869e307b 100644
--- a/lisp/eshell/esh-io.el
+++ b/lisp/eshell/esh-io.el
@@ -144,9 +144,8 @@ eshell-virtual-targets
 output function.  Otherwise, the second element itself is the output
 function.
 
-The output function is then called repeatedly with single strings,
-which represents successive pieces of the output of the command, until nil
-is passed, meaning EOF."
+When using the virtual target, Eshell calls the output function
+repeatedly with each object to output."
   :version "30.1"
   :type '(repeat
 	  (list (string :tag "Target")
@@ -158,6 +157,12 @@ eshell-virtual-targets
 
 (define-error 'eshell-pipe-broken "Pipe broken")
 
+(defvar eshell-ensure-newline-p nil
+  "If non-nil, ensure that a newline is emitted after a Lisp form.
+This can be changed by Lisp forms that are evaluated from the
+Eshell command line.  This behavior only applies to line-oriented
+output targets (see `eshell-target-line-oriented-p'.")
+
 ;;; Internal Variables:
 
 (defconst eshell-redirection-operators-alist
@@ -489,20 +494,48 @@ eshell-error
   "Output OBJECT to the standard error handle."
   (eshell-output-object object eshell-error-handle))
 
-(defsubst eshell-errorn (object)
-  "Output OBJECT followed by a newline to the standard error handle."
-  (eshell-error object)
-  (eshell-error "\n"))
-
 (defsubst eshell-printn (object)
   "Output OBJECT followed by a newline to the standard output handle."
   (eshell-print object)
   (eshell-print "\n"))
 
-(cl-defstruct (eshell-virtual-target
+(defsubst eshell-errorn (object)
+  "Output OBJECT followed by a newline to the standard error handle."
+  (eshell-error object)
+  (eshell-error "\n"))
+
+(defun eshell--output-maybe-n (object handle)
+  "Output OBJECT to HANDLE.
+For any line-oriented output targets on HANDLE, ensure the output
+ends in a newline."
+  (eshell-output-object object handle)
+  (when (and eshell-ensure-newline-p
+             (not (and (stringp object)
+                       (string-suffix-p object "\n"))))
+    (eshell-maybe-output-newline handle)))
+
+(defsubst eshell-print-maybe-n (object)
+  "Output OBJECT to the standard output handle.
+For any line-oriented output targets, ensure the output ends in a
+newline."
+  (eshell--output-maybe-n object eshell-output-handle))
+
+(defsubst eshell-error-maybe-n (object)
+  "Output OBJECT to the standard error handle.
+For any line-oriented output targets, ensure the output ends in a
+newline."
+  (eshell--output-maybe-n object eshell-error-handle))
+
+(cl-defstruct (eshell-generic-target (:constructor nil))
+  "An Eshell target.
+This is mainly useful for creating virtual targets (see
+`eshell-virtual-targets').")
+
+(cl-defstruct (eshell-function-target
+               (:include eshell-generic-target)
                (:constructor nil)
-               (:constructor eshell-virtual-target-create (output-function)))
-  "A virtual target (see `eshell-virtual-targets')."
+               (:constructor eshell-function-target-create (output-function)))
+  "An Eshell target that calls an output function."
   output-function)
 
 (cl-defgeneric eshell-get-target (raw-target &optional _mode)
@@ -514,14 +547,16 @@ eshell-get-target
 (cl-defmethod eshell-get-target ((raw-target string) &optional mode)
   "Convert a string RAW-TARGET into a valid output target using MODE.
 If TARGET is a virtual target (see `eshell-virtual-targets'),
-return an `eshell-virtual-target' instance; otherwise, return a
+return an `eshell-generic-target' instance; otherwise, return a
 marker for a file named TARGET."
   (setq mode (or mode 'insert))
   (if-let ((redir (assoc raw-target eshell-virtual-targets)))
-      (eshell-virtual-target-create
-       (if (nth 2 redir)
-           (funcall (nth 1 redir) mode)
-         (nth 1 redir)))
+      (let ((target (if (nth 2 redir)
+                        (funcall (nth 1 redir) mode)
+                      (nth 1 redir))))
+        (unless (eshell-generic-target-p target)
+          (setq target (eshell-function-target-create target)))
+        target)
     (let ((exists (get-file-buffer raw-target))
           (buf (find-file-noselect raw-target t)))
       (with-current-buffer buf
@@ -602,8 +637,8 @@ eshell-close-target
         (throw 'done nil))
       (process-send-eof target))))
 
-(cl-defmethod eshell-close-target ((_target eshell-virtual-target) _status)
-  "Close a virtual TARGET."
+(cl-defmethod eshell-close-target ((_target eshell-function-target) _status)
+  "Close an Eshell function TARGET."
   nil)
 
 (cl-defgeneric eshell-output-object-to-target (object target)
@@ -660,9 +695,19 @@ eshell-output-object-to-target
   object)
 
 (cl-defmethod eshell-output-object-to-target (object
-                                              (target eshell-virtual-target))
-  "Output OBJECT to the virtual TARGET."
-  (funcall (eshell-virtual-target-output-function target) object))
+                                              (target eshell-function-target))
+  "Output OBJECT to the Eshell function TARGET."
+  (funcall (eshell-function-target-output-function target) object))
+
+(cl-defgeneric eshell-target-line-oriented-p (_target)
+  "Return non-nil if the specified TARGET is line-oriented.
+Line-oriented targets are those that expect a newline after
+command output when `eshell-ensure-newline-p' is non-nil."
+  nil)
+
+(cl-defmethod eshell-target-line-oriented-p ((_target (eql t)))
+  "Return non-nil to indicate that the display is line-oriented."
+  t)
 
 (defun eshell-output-object (object &optional handle-index handles)
   "Insert OBJECT, using HANDLE-INDEX specifically.
@@ -674,5 +719,18 @@ eshell-output-object
     (dolist (target targets)
       (eshell-output-object-to-target object target))))
 
+(defun eshell-maybe-output-newline (&optional handle-index handles)
+  "Maybe insert a newline, using HANDLE-INDEX specifically.
+This inserts a newline for all line-oriented output targets.
+
+If HANDLE-INDEX is nil, output to `eshell-output-handle'.
+HANDLES is the set of file handles to use; if nil, use
+`eshell-current-handles'."
+  (let ((targets (caar (aref (or handles eshell-current-handles)
+                             (or handle-index eshell-output-handle)))))
+    (dolist (target targets)
+      (when (eshell-target-line-oriented-p target)
+        (eshell-output-object-to-target "\n" target)))))
+
 (provide 'esh-io)
 ;;; esh-io.el ends here
-- 
2.25.1

From 3cb8cf0229fc154e439a0a27c047d42ce8452a3b Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Mon, 20 May 2024 08:59:02 -0700
Subject: [PATCH 4/4] Fix calling Eshell scripts outside of Eshell

* lisp/eshell/em-script.el (eshell-source-file): Make obsolete.
(eshell--source-file): Adapt from 'eshell-source-file'...
(eshell-script-initialize, eshell/source, eshell/.): ... use it.
(eshell-princ-target): New struct.
(eshell-output-object-to-target, eshell-target-line-oriented-p): New
implementations for 'eshell-princ-target'.
(eshell-execute-file, eshell-batch-file): New functions.

* lisp/eshell/esh-mode.el (eshell-mode): Just warn if we can't create
the Eshell directory.

* test/lisp/eshell/em-script-tests.el (em-script-test/execute-file):
(em-script-test/execute-file/args), em-script-test/batch-file): New
tests.

* test/lisp/eshell/eshell-tests-helpers.el (with-temp-eshell-settings):
New function...
(with-temp-eshell): ... use it.

* doc/misc/eshell.texi (Control Flow): Update documentation.
---
 doc/misc/eshell.texi                     |  15 ++-
 lisp/eshell/em-script.el                 | 115 ++++++++++++++++++-----
 lisp/eshell/esh-mode.el                  |   3 +-
 test/lisp/eshell/em-script-tests.el      |  31 ++++++
 test/lisp/eshell/eshell-tests-helpers.el |  36 ++++---
 5 files changed, 155 insertions(+), 45 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index bdf19bb6bd9..ae76546c52f 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -1686,13 +1686,20 @@ Control Flow
 @node Scripts
 @section Scripts
 @cmindex source
-@fnindex eshell-source-file
+@fnindex eshell-execute-file
+@fnindex eshell-batch-file
 You can run Eshell scripts much like scripts for other shells; the main
 difference is that since Eshell is not a system command, you have to run
 it from within Emacs.  An Eshell script is simply a file containing a
-sequence of commands, as with almost any other shell script.  Scripts
-are invoked from Eshell with @command{source}, or from anywhere in Emacs
-with @code{eshell-source-file}.
+sequence of commands, as with almost any other shell script.  You can
+invoke scripts from within Eshell with @command{source}, or from
+anywhere in Emacs with @code{eshell-execute-file}.  Additionally, you
+can make an Eshell script file executable by calling
+@code{eshell-batch-file} in the interpreter directive:
+
+@example
+#!/usr/bin/env -S emacs --batch -f eshell-batch-file
+@end example
 
 Like with aliases (@pxref{Aliases}), Eshell scripts can accept any
 number of arguments.  Within the script, you can refer to these with
diff --git a/lisp/eshell/em-script.el b/lisp/eshell/em-script.el
index 254a11ea114..6e2ca7ca781 100644
--- a/lisp/eshell/em-script.el
+++ b/lisp/eshell/em-script.el
@@ -24,6 +24,7 @@
 ;;; Code:
 
 (require 'esh-mode)
+(require 'esh-io)
 
 ;;;###esh-module-autoload
 (progn
@@ -75,42 +76,106 @@ eshell-script-initialize
 	 eshell-login-script
 	 (file-readable-p eshell-login-script)
 	 (eshell-do-eval
-	  (list 'eshell-commands
-		(catch 'eshell-replace-command
-		  (eshell-source-file eshell-login-script)))
+	  `(eshell-commands ,(eshell--source-file eshell-login-script))
           t))
     (and eshell-rc-script
 	 (file-readable-p eshell-rc-script)
 	 (eshell-do-eval
-	  (list 'eshell-commands
-		(catch 'eshell-replace-command
-		  (eshell-source-file eshell-rc-script))) t))))
+	  `(eshell-commands ,(eshell--source-file eshell-rc-script))
+          t))))
 
-(defun eshell-source-file (file &optional args subcommand-p)
-  "Execute a series of Eshell commands in FILE, passing ARGS.
-Comments begin with `#'."
+(defun eshell--source-file (file &optional args subcommand-p)
+  "Return a Lisp form for executig the Eshell commands in FILE, passing ARGS.
+If SUBCOMMAND-P is non-nil, execute this as a subcommand."
   (let ((cmd (eshell-parse-command `(:file . ,file))))
     (when subcommand-p
       (setq cmd `(eshell-as-subcommand ,cmd)))
-    (throw 'eshell-replace-command
-           `(let ((eshell-command-name ',file)
-                  (eshell-command-arguments ',args)
-                  ;; Don't print subjob messages by default.
-                  ;; Otherwise, if this function was called as a
-                  ;; subjob, then *all* commands in the script would
-                  ;; print start/stop messages.
-                  (eshell-subjob-messages nil))
-              ,cmd))))
-
-(defun eshell/source (&rest args)
-  "Source a file in a subshell environment."
-  (eshell-source-file (car args) (cdr args) t))
+    `(let ((eshell-command-name ',file)
+           (eshell-command-arguments ',args)
+           ;; Don't print subjob messages by default.  Otherwise, if
+           ;; this function was called as a subjob, then *all* commands
+           ;; in the script would print start/stop messages.
+           (eshell-subjob-messages nil))
+       ,cmd)))
+
+(defun eshell-source-file (file &optional args subcommand-p)
+  "Execute a series of Eshell commands in FILE, passing ARGS.
+Comments begin with `#'."
+  (declare (obsolete nil "30.1"))
+  (throw 'eshell-replace-command
+         (eshell--source-file file args subcommand-p)))
+
+;;;###autoload
+(defun eshell-execute-file (file &optional args destination)
+  "Execute a series of Eshell commands in FILE, passing ARGS.
+Comments begin with `#'."
+  (let ((eshell-non-interactive-p t)
+        (stdout (if (eq destination t) (current-buffer) destination)))
+    (with-temp-buffer
+      (eshell-mode)
+      (eshell-do-eval
+       `(let ((eshell-current-handles
+               (eshell-create-handles ,stdout 'insert))
+              (eshell-current-subjob-p))
+          ,(eshell--source-file file args))
+       t))))
+
+(cl-defstruct (eshell-princ-target
+               (:include eshell-generic-target)
+               (:constructor nil)
+               (:constructor eshell-princ-target-create
+                             (&optional printcharfun)))
+  "A virtual target calling `princ' (see `eshell-virtual-targets')."
+  printcharfun)
+
+(cl-defmethod eshell-output-object-to-target (object
+                                              (target eshell-princ-target))
+  "Output OBJECT to the `princ' function TARGET."
+  (princ object (eshell-princ-target-printcharfun target)))
+
+(cl-defmethod eshell-target-line-oriented-p ((_target eshell-princ-target))
+  "Return non-nil to indicate that the display is line-oriented."
+  t)
+
+;;;###autoload
+(defun eshell-batch-file ()
+  "Execute an Eshell script as a batch script from the command line.
+Inside your Eshell script file, you can add the following at the
+top in order to make it into an executable script:
+
+  #!/usr/bin/env -S emacs --batch -f eshell-batch-file"
+  (let ((file (pop command-line-args-left))
+        (args command-line-args-left)
+        (eshell-non-interactive-p t)
+        (eshell-module-loading-messages nil)
+        (eshell-virtual-targets
+         (append `(("/dev/stdout" ,(eshell-princ-target-create) nil)
+                   ("/dev/stderr" ,(eshell-princ-target-create
+                                    #'external-debugging-output)
+                   nil))
+                 eshell-virtual-targets)))
+    (setq command-line-args-left nil)
+    (with-temp-buffer
+      (eshell-mode)
+      (eshell-do-eval
+       `(let ((eshell-current-handles
+               (eshell-create-handles "/dev/stdout" 'append
+                                      "/dev/stderr" 'append))
+              (eshell-current-subjob-p))
+          ,(eshell--source-file file args))
+       t))))
+
+(defun eshell/source (file &rest args)
+  "Source a FILE in a subshell environment."
+  (throw 'eshell-replace-command
+         (eshell--source-file file args t)))
 
 (put 'eshell/source 'eshell-no-numeric-conversions t)
 
-(defun eshell/. (&rest args)
-  "Source a file in the current environment."
-  (eshell-source-file (car args) (cdr args)))
+(defun eshell/. (file &rest args)
+  "Source a FILE in the current environment."
+  (throw 'eshell-replace-command
+         (eshell--source-file file args)))
 
 (put 'eshell/. 'eshell-no-numeric-conversions t)
 
diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 31a9216be33..488ff58352e 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -376,7 +376,8 @@ eshell-mode
   (eshell-load-modules eshell-modules-list)
 
   (unless (file-exists-p eshell-directory-name)
-    (eshell-make-private-directory eshell-directory-name t))
+    (with-demoted-errors "Error creating Eshell directory: %s"
+      (eshell-make-private-directory eshell-directory-name t)))
 
   ;; Initialize core Eshell modules, then extension modules, for this session.
   (eshell-initialize-modules (eshell-subgroups 'eshell))
diff --git a/test/lisp/eshell/em-script-tests.el b/test/lisp/eshell/em-script-tests.el
index f77c4568ea8..5ec64ebe089 100644
--- a/test/lisp/eshell/em-script-tests.el
+++ b/test/lisp/eshell/em-script-tests.el
@@ -24,6 +24,7 @@
 ;;; Code:
 
 (require 'ert)
+(require 'ert-x)
 (require 'esh-mode)
 (require 'eshell)
 (require 'em-script)
@@ -94,4 +95,34 @@ em-script-test/source-script/all-args-var
      (eshell-match-command-output (format "source %s a b c" temp-file)
                                   "a\nb\nc\n"))))
 
+(ert-deftest em-script-test/execute-file ()
+  "FIXME"
+  (ert-with-temp-file temp-file
+    :text "echo hi\necho bye"
+    (with-temp-buffer
+      (with-temp-eshell-settings
+        (eshell-execute-file temp-file nil t))
+      (should (equal (buffer-string) "hibye")))))
+
+(ert-deftest em-script-test/execute-file/args ()
+  "FIXME"
+  (ert-with-temp-file temp-file
+    :text "+ $@*"
+    (with-temp-buffer
+      (with-temp-eshell-settings
+        (eshell-execute-file temp-file '(1 2 3) t))
+      (should (equal (buffer-string) "6")))))
+
+(ert-deftest em-script-test/batch-file ()
+  "FIXME"
+  (ert-with-temp-file temp-file
+    :text (format
+           "#!/usr/bin/env -S %s -Q --batch -f eshell-batch-file\necho hi"
+           (expand-file-name invocation-name invocation-directory))
+    (set-file-modes temp-file #o744)
+    (with-temp-buffer
+      (with-temp-eshell-settings
+        (call-process temp-file nil '(t nil)))
+      (should (equal (buffer-string) "hi\n")))))
+
 ;; em-script-tests.el ends here
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
index 3f1c55f420d..a15fe611676 100644
--- a/test/lisp/eshell/eshell-tests-helpers.el
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -47,24 +47,30 @@ eshell-tests-remote-accessible-p
      (file-directory-p ert-remote-temporary-file-directory)
      (file-writable-p ert-remote-temporary-file-directory))))
 
+(defmacro with-temp-eshell-settings (&rest body)
+  "Configure Eshell to leave no trace behind, and then evaluate BODY."
+  (declare (indent 0))
+  `(ert-with-temp-directory eshell-directory-name
+     (let (;; We want no history file, so prevent Eshell from falling
+           ;; back on $HISTFILE.
+           (process-environment (cons "HISTFILE" process-environment))
+           ;; Enable process debug instrumentation.  We may be able to
+           ;; remove this eventually once we're confident that all the
+           ;; process bugs have been worked out.  (At that point, we can
+           ;; just enable this selectively when needed.)  See also
+           ;; `eshell-test-command-result' below.
+           (eshell-debug-command (cons 'process eshell-debug-command))
+           (eshell-history-file-name nil)
+           (eshell-last-dir-ring-file-name nil)
+           (eshell-module-loading-messages nil))
+       ,@body)))
+
 (defmacro with-temp-eshell (&rest body)
   "Evaluate BODY in a temporary Eshell buffer."
+  (declare (indent 0))
   `(save-current-buffer
-     (ert-with-temp-directory eshell-directory-name
-       (let* (;; We want no history file, so prevent Eshell from falling
-              ;; back on $HISTFILE.
-              (process-environment (cons "HISTFILE" process-environment))
-              ;; Enable process debug instrumentation.  We may be able
-              ;; to remove this eventually once we're confident that
-              ;; all the process bugs have been worked out.  (At that
-              ;; point, we can just enable this selectively when
-              ;; needed.)  See also `eshell-test-command-result'
-              ;; below.
-              (eshell-debug-command (cons 'process eshell-debug-command))
-              (eshell-history-file-name nil)
-              (eshell-last-dir-ring-file-name nil)
-              (eshell-module-loading-messages nil)
-              (eshell-buffer (eshell t)))
+     (with-temp-eshell-settings
+       (let ((eshell-buffer (eshell t)))
          (unwind-protect
              (with-current-buffer eshell-buffer
                ,@body)
-- 
2.25.1