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) + "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) 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)