[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
master 9280a619ab3 5/5: Fix calling Eshell scripts outside of Eshell
From: |
Jim Porter |
Subject: |
master 9280a619ab3 5/5: Fix calling Eshell scripts outside of Eshell |
Date: |
Wed, 29 May 2024 15:21:41 -0400 (EDT) |
branch: master
commit 9280a619ab3141c0b3b8f4ae876f82e6a38c757f
Author: Jim Porter <jporterbugs@gmail.com>
Commit: Jim Porter <jporterbugs@gmail.com>
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.
* etc/NEWS: Announce this change (bug#70847).
---
doc/misc/eshell.texi | 15 ++--
etc/NEWS | 7 ++
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 ++++++----
6 files changed, 162 insertions(+), 45 deletions(-)
diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 2da132e01eb..873d14aff32 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -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
diff --git a/etc/NEWS b/etc/NEWS
index 9416ced5a0d..c9334e18e2d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -870,6 +870,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
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 @@ 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)
diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 7290c29b008..7c030639955 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -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))
diff --git a/test/lisp/eshell/em-script-tests.el
b/test/lisp/eshell/em-script-tests.el
index f77c4568ea8..f3adbae9df7 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 @@
(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
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 @@ 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)