]> git.eshelyaron.com Git - emacs.git/commitdiff
Fix calling Eshell scripts outside of Eshell
authorJim Porter <jporterbugs@gmail.com>
Mon, 20 May 2024 15:59:02 +0000 (08:59 -0700)
committerEshel Yaron <me@eshelyaron.com>
Thu, 30 May 2024 14:27:44 +0000 (16:27 +0200)
* 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.

* etc/NEWS: Announce this change (bug#70847).

(cherry picked from commit 9280a619ab3141c0b3b8f4ae876f82e6a38c757f)

doc/misc/eshell.texi
etc/NEWS
lisp/eshell/em-script.el
lisp/eshell/esh-mode.el
test/lisp/eshell/em-script-tests.el
test/lisp/eshell/eshell-tests-helpers.el

index 2da132e01eb74ae7ad223e39a574f748fc941ef0..873d14aff32b95c9a44e60af55cb0f9ef5cf1a84 100644 (file)
@@ -1686,13 +1686,20 @@ treat it as a list of one element.  If you specify multiple
 @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
index b1161d6b5c7bf6349f9751260e84728275762875..494f1ecc063241b581a3a50a4f7a2557110526f6 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -847,6 +847,13 @@ using this new option.  (Or set 'display-buffer-alist' directly.)
 
 ** Eshell
 
++++
+*** You can now run Eshell scripts in batch mode.
+By adding the following interpreter directive to an Eshell script, you
+can make it executable like other shell scripts:
+
+    #!/usr/bin/env -S emacs --batch -f eshell-batch-file
+
 +++
 *** New builtin Eshell command 'compile'.
 This command runs another command, sending its output to a compilation
index 254a11ea1144ef205844f74dc0d600f88117c54e..6e2ca7ca781751e62736898dcfefc1e832f47b1d 100644 (file)
@@ -24,6 +24,7 @@
 ;;; Code:
 
 (require 'esh-mode)
+(require 'esh-io)
 
 ;;;###esh-module-autoload
 (progn
@@ -75,42 +76,106 @@ This includes when running `eshell-command'."
         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)
 
index 7290c29b00809cad1dfcb673d8ede9279ce03325..7c03063995520659862222a29ab62645013a1687 100644 (file)
@@ -376,7 +376,8 @@ and the hook `eshell-exit-hook'."
   (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))
index f77c4568ea8f3f3f10248b69ed94d41aaba79f06..f3adbae9df785055f609f1e11e85436831073f62 100644 (file)
@@ -24,6 +24,7 @@
 ;;; Code:
 
 (require 'ert)
+(require 'ert-x)
 (require 'esh-mode)
 (require 'eshell)
 (require 'em-script)
      (eshell-match-command-output (format "source %s a b c" temp-file)
                                   "a\nb\nc\n"))))
 
+(ert-deftest em-script-test/execute-file ()
+  "Test running an Eshell script file via `eshell-execute-file'."
+  (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 ()
+  "Test running an Eshell script file with args via `eshell-execute-file'."
+  (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 ()
+  "Test running an Eshell script file as a batch script."
+  (ert-with-temp-file temp-file
+    :text (format
+           "#!/usr/bin/env -S %s --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
index 3f1c55f420dec530b3582b7a05b29b4d74c69be1..a15fe61167659113a345a96c419d2cc0ab5995bc 100644 (file)
@@ -47,24 +47,30 @@ beginning of the test file."
      (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)