1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-11-23 07:19:15 +00:00

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).
This commit is contained in:
Jim Porter 2024-05-20 08:59:02 -07:00
parent eac608cb80
commit 9280a619ab
6 changed files with 161 additions and 44 deletions

View 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

View File

@ -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

View 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)
"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)))
`(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 `#'."
(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))))
(declare (obsolete nil "30.1"))
(throw 'eshell-replace-command
(eshell--source-file file args subcommand-p)))
(defun eshell/source (&rest args)
"Source a file in a subshell environment."
(eshell-source-file (car args) (cdr args) t))
;;;###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)

View 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))

View File

@ -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

View 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)