summaryrefslogtreecommitdiff
path: root/lisp/eshell
diff options
context:
space:
mode:
authorJim Porter <jporterbugs@gmail.com>2023-09-23 11:36:11 -0700
committerJim Porter <jporterbugs@gmail.com>2023-10-02 20:49:41 -0700
commit498d31e9f0549189f4e9b140549419dd4e462575 (patch)
treee66ae54cfe9f53d7481df35850a066042e0b6e1d /lisp/eshell
parent8f2cfe15a72a0c440909faa50a9c436931dcf85e (diff)
downloademacs-498d31e9f0549189f4e9b140549419dd4e462575.tar.gz
Support Eshell iterative evaluation in the background
This really just generalizes Eshell's previous support for iterative evaluation of a single current command to a list of multiple commands, of which at most one can be in the foreground (bug#66066). * lisp/eshell/esh-cmd.el (eshell-last-async-procs) (eshell-current-command): Make obsolete in favor of... (eshell-foreground-command): ... this (eshell-background-commands): New variable. (eshell-interactive-process-p): Make obsolete. (eshell-head-process, eshell-tail-process): Use 'eshell-foreground-command'. (eshell-cmd-initialize): Initialize new variables. (eshell-add-command, eshell-remove-command) (eshell-commands-for-process): New functions. (eshell-parse-command): Make 'eshell-do-subjob' the outermost call. (eshell-do-subjob): Call 'eshell-resume-eval' to split this command off from its parent forms. (eshell-eval-command): Use 'eshell-add-command'. (eshell-resume-command): Use 'eshell-commands-for-process'. (eshell-resume-eval): Take a COMMAND argument. Return ':eshell-background' form for deferred background commands. (eshell-do-eval): Remove check for 'eshell-current-subjob-p'. This is handled differently now. * lisp/eshell/eshell.el (eshell-command): Wait for all processes to exit when running synchronously. * lisp/eshell/esh-mode.el (eshell-intercept-commands) (eshell-watch-for-password-prompt): * lisp/eshell/em-cmpl.el (eshell-complete-parse-arguments): * lisp/eshell/em-smart.el (eshell-smart-display-move): Use 'eshell-foreground-command'. * test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/background/simple-command) (esh-cmd-test/background/subcommand): New tests. (esh-cmd-test/throw): Use 'eshell-foreground-command'. * test/lisp/eshell/eshell-tests.el (eshell-test/queue-input): Use 'eshell-foreground-command'. * test/lisp/eshell/em-script-tests.el (em-script-test/source-script/background): Make the test script more complex. * test/lisp/eshell/eshell-tests.el (eshell-test/eshell-command/pipeline-wait): New test. * doc/misc/eshell.texi (Bugs and ideas): Remove implemented feature.
Diffstat (limited to 'lisp/eshell')
-rw-r--r--lisp/eshell/em-cmpl.el2
-rw-r--r--lisp/eshell/em-smart.el2
-rw-r--r--lisp/eshell/esh-cmd.el176
-rw-r--r--lisp/eshell/esh-mode.el4
-rw-r--r--lisp/eshell/eshell.el5
5 files changed, 123 insertions, 66 deletions
diff --git a/lisp/eshell/em-cmpl.el b/lisp/eshell/em-cmpl.el
index 25dccbd695c..61f1237b907 100644
--- a/lisp/eshell/em-cmpl.el
+++ b/lisp/eshell/em-cmpl.el
@@ -343,7 +343,7 @@ to writing a completion function."
(defun eshell-complete-parse-arguments ()
"Parse the command line arguments for `pcomplete-argument'."
(when (and eshell-no-completion-during-jobs
- (eshell-interactive-process-p))
+ eshell-foreground-command)
(eshell--pcomplete-insert-tab))
(let ((end (point-marker))
(begin (save-excursion (beginning-of-line) (point)))
diff --git a/lisp/eshell/em-smart.el b/lisp/eshell/em-smart.el
index d5002a59d14..4c39a991ec6 100644
--- a/lisp/eshell/em-smart.el
+++ b/lisp/eshell/em-smart.el
@@ -294,7 +294,7 @@ and the end of the buffer are still visible."
((eq this-command 'self-insert-command)
(if (eq last-command-event ? )
(if (and eshell-smart-space-goes-to-end
- eshell-current-command)
+ eshell-foreground-command)
(if (not (pos-visible-in-window-p (point-max)))
(setq this-command 'scroll-up)
(setq this-command 'eshell-smart-goto-end))
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index fc7d54a758d..990d2ca1122 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -263,7 +263,24 @@ command line.")
;;; Internal Variables:
-(defvar eshell-current-command nil)
+;; These variables have been merged into `eshell-foreground-command'.
+;; Outside of this file, the most-common use for them is to check
+;; whether they're nil.
+(define-obsolete-variable-alias 'eshell-last-async-procs
+ 'eshell-foreground-command "30.1")
+(define-obsolete-variable-alias 'eshell-current-command
+ 'eshell-foreground-command "30.1")
+
+(defvar eshell-foreground-command nil
+ "The currently-running foreground command, if any.
+This is a list of the form (FORM PROCESSES). FORM is the Eshell
+command form. PROCESSES is a list of processes that deferred the
+command.")
+(defvar eshell-background-commands nil
+ "A list of currently-running deferred commands.
+Each element is of the form (FORM PROCESSES), as with
+`eshell-foreground-command' (which see).")
+
(defvar eshell-command-name nil)
(defvar eshell-command-arguments nil)
(defvar eshell-in-pipeline-p nil
@@ -273,11 +290,6 @@ otherwise t.")
(defvar eshell-in-subcommand-p nil)
(defvar eshell-last-arguments nil)
(defvar eshell-last-command-name nil)
-(defvar eshell-last-async-procs nil
- "The currently-running foreground process(es).
-When executing a pipeline, this is a list of all the pipeline's
-processes, with the first usually reading from stdin and last
-usually writing to stdout.")
(defvar eshell-allow-commands t
"If non-nil, allow evaluating command forms (including Lisp forms).
@@ -294,29 +306,30 @@ also `eshell-complete-parse-arguments'.")
(defsubst eshell-interactive-process-p ()
"Return non-nil if there is a currently running command process."
- eshell-last-async-procs)
+ (declare (obsolete 'eshell-foreground-command "30.1"))
+ eshell-foreground-command)
(defsubst eshell-head-process ()
"Return the currently running process at the head of any pipeline.
This only returns external (non-Lisp) processes."
- (car eshell-last-async-procs))
+ (caadr eshell-foreground-command))
(defsubst eshell-tail-process ()
"Return the currently running process at the tail of any pipeline.
This only returns external (non-Lisp) processes."
- (car (last eshell-last-async-procs)))
+ (car (last (cadr eshell-foreground-command))))
(define-obsolete-function-alias 'eshell-interactive-process
'eshell-tail-process "29.1")
(defun eshell-cmd-initialize () ;Called from `eshell-mode' via intern-soft!
"Initialize the Eshell command processing module."
- (setq-local eshell-current-command nil)
+ (setq-local eshell-foreground-command nil)
+ (setq-local eshell-background-commands nil)
(setq-local eshell-command-name nil)
(setq-local eshell-command-arguments nil)
(setq-local eshell-last-arguments nil)
(setq-local eshell-last-command-name nil)
- (setq-local eshell-last-async-procs nil)
(add-hook 'eshell-kill-hook #'eshell-resume-command nil t)
(add-hook 'eshell-parse-argument-hook
@@ -337,6 +350,47 @@ This only returns external (non-Lisp) processes."
(throw 'pcomplete-completions
(all-completions pcomplete-stub obarray 'boundp)))))
+;; Current command management
+
+(defun eshell-add-command (form &optional background)
+ "Add a command FORM to our list of known commands and return the new entry.
+If non-nil, BACKGROUND indicates that this is a command running
+in the background. The result is a command entry in the
+form (BACKGROUND FORM PROCESSES), where PROCESSES is initially
+nil."
+ (cons (when background 'background)
+ (if background
+ (car (push (list form nil) eshell-background-commands))
+ (cl-assert (null eshell-foreground-command))
+ (setq eshell-foreground-command (list form nil)))))
+
+(defun eshell-remove-command (command)
+ "Remove COMMAND from our list of known commands.
+COMMAND should be a list of the form (BACKGROUND FORM PROCESSES),
+as returned by `eshell-add-command' (which see)."
+ (let ((background (car command))
+ (entry (cdr command)))
+ (if background
+ (setq eshell-background-commands
+ (delq entry eshell-background-commands))
+ (cl-assert (eq eshell-foreground-command entry))
+ (setq eshell-foreground-command nil))))
+
+(defun eshell-commands-for-process (process)
+ "Return all commands associated with a PROCESS.
+Each element will have the form (BACKGROUND FORM PROCESSES), as
+returned by `eshell-add-command' (which see).
+
+Usually, there should only be one element in this list, but it's
+theoretically possible to have more than one associated command
+for a given process."
+ (nconc (when (memq process (cadr eshell-foreground-command))
+ (list (cons nil eshell-foreground-command)))
+ (seq-keep (lambda (cmd)
+ (when (memq process (cadr cmd))
+ (cons 'background cmd)))
+ eshell-background-commands)))
+
;; Command parsing
(defsubst eshell--region-p (object)
@@ -407,8 +461,6 @@ command hooks should be run before and after the command."
(lambda (cmd)
(let ((sep (pop sep-terms)))
(setq cmd (eshell-parse-pipeline cmd))
- (when (equal sep "&")
- (setq cmd `(eshell-do-subjob (cons :eshell-background ,cmd))))
(unless eshell-in-pipeline-p
(setq cmd `(eshell-trap-errors ,cmd)))
;; Copy I/O handles so each full statement can manipulate
@@ -416,6 +468,8 @@ command hooks should be run before and after the command."
;; command in the list; we won't use the originals again
;; anyway.
(setq cmd `(eshell-with-copied-handles ,cmd ,(not sep)))
+ (when (equal sep "&")
+ (setq cmd `(eshell-do-subjob ,cmd)))
cmd))
sub-chains)))
(if toplevel
@@ -740,13 +794,13 @@ if none)."
(defmacro eshell-do-subjob (object)
"Evaluate a command OBJECT as a subjob.
-We indicate that the process was run in the background by returning it
-ensconced in a list."
+We indicate that the process was run in the background by
+returning it as (:eshell-background . PROCESSES)."
`(let ((eshell-current-subjob-p t)
;; Print subjob messages. This could have been cleared
;; (e.g. by `eshell-source-file', which see).
(eshell-subjob-messages t))
- ,object))
+ (eshell-resume-eval (eshell-add-command ',object 'background))))
(defmacro eshell-commands (object &optional silent)
"Place a valid set of handles, and context, around command OBJECT."
@@ -980,12 +1034,12 @@ Return the process (or head and tail processes) created by
COMMAND, if any. If COMMAND is a background command, return the
process(es) in a cons cell like:
- (:eshell-background . PROCESS)"
- (if eshell-current-command
+ (:eshell-background . PROCESSES)"
+ (if eshell-foreground-command
(progn
;; We can just stick the new command at the end of the current
;; one, and everything will happen as it should.
- (setcdr (last (cdr eshell-current-command))
+ (setcdr (last (cdar eshell-foreground-command))
(list `(let ((here (and (eobp) (point))))
,(and input
`(insert-and-inherit ,(concat input "\n")))
@@ -994,56 +1048,61 @@ process(es) in a cons cell like:
(eshell-do-eval ',command))))
(eshell-debug-command 'form
"enqueued command form for %S\n\n%s"
- (or input "<no string>") (eshell-stringify eshell-current-command)))
+ (or input "<no string>")
+ (eshell-stringify (car eshell-foreground-command))))
(eshell-debug-command-start input)
- (setq eshell-current-command command)
(let* (result
(delim (catch 'eshell-incomplete
- (ignore (setq result (eshell-resume-eval))))))
+ (ignore (setq result (eshell-resume-eval
+ (eshell-add-command command)))))))
(when delim
(error "Unmatched delimiter: %S" delim))
result)))
(defun eshell-resume-command (proc status)
- "Resume the current command when a pipeline ends."
- (when (and proc
- ;; Make sure PROC is one of our foreground processes and
- ;; that all of those processes are now dead.
- (member proc eshell-last-async-procs)
- (not (seq-some #'eshell-process-active-p eshell-last-async-procs)))
- (if (and ;; Check STATUS to determine whether we want to resume or
- ;; abort the command.
- (stringp status)
- (not (string= "stopped" status))
- (not (string-match eshell-reset-signals status)))
- (eshell-resume-eval)
- (setq eshell-last-async-procs nil)
- (setq eshell-current-command nil)
- (declare-function eshell-reset "esh-mode" (&optional no-hooks))
- (eshell-reset))))
-
-(defun eshell-resume-eval ()
- "Destructively evaluate a form which may need to be deferred."
- (setq eshell-last-async-procs nil)
- (when eshell-current-command
- (eshell-condition-case err
- (let (retval procs)
- (unwind-protect
- (progn
- (setq procs (catch 'eshell-defer
- (ignore (setq retval
- (eshell-do-eval
- eshell-current-command)))))
- (when retval
- (cadr retval)))
- (setq eshell-last-async-procs procs)
+ "Resume the current command when a pipeline ends.
+PROC is the process that invoked this from its sentinel, and
+STATUS is its status."
+ (when proc
+ (dolist (command (eshell-commands-for-process proc))
+ (unless (seq-some #'eshell-process-active-p (nth 2 command))
+ (setf (nth 2 command) nil) ; Clear processes from command.
+ (if (and ;; Check STATUS to determine whether we want to resume or
+ ;; abort the command.
+ (stringp status)
+ (not (string= "stopped" status))
+ (not (string-match eshell-reset-signals status)))
+ (eshell-resume-eval command)
+ (eshell-remove-command command)
+ (declare-function eshell-reset "esh-mode" (&optional no-hooks))
+ (eshell-reset))))))
+
+(defun eshell-resume-eval (command)
+ "Destructively evaluate a COMMAND which may need to be deferred.
+COMMAND is a command entry of the form (BACKGROUND FORM
+PROCESSES) (see `eshell-add-command').
+
+Return the result of COMMAND's FORM if it wasn't deferred. If
+BACKGROUND is non-nil and Eshell defers COMMAND, return a list of
+the form (:eshell-background . PROCESSES)."
+ (eshell-condition-case err
+ (let (retval procs)
+ (unwind-protect
+ (progn
+ (setq procs
+ (catch 'eshell-defer
+ (ignore (setq retval (eshell-do-eval (cadr command))))))
+ (cond
+ (retval (cadr retval))
+ ((car command) (cons :eshell-background procs))))
+ (if procs
+ (setf (nth 2 command) procs)
;; If we didn't defer this command, clear it out. This
;; applies both when the command has finished normally,
;; and when a signal or thrown value causes us to unwind.
- (unless procs
- (setq eshell-current-command nil))))
- (error
- (error (error-message-string err))))))
+ (eshell-remove-command command))))
+ (error
+ (error (error-message-string err)))))
(defmacro eshell-manipulate (form tag &rest body)
"Manipulate a command FORM with BODY, using TAG as a debug identifier."
@@ -1272,7 +1331,6 @@ have been replaced by constants."
(setcdr form (cdr new-form)))
(eshell-do-eval form synchronous-p))
(if-let (((memq (car form) eshell-deferrable-commands))
- ((not eshell-current-subjob-p))
(procs (eshell-make-process-list result)))
(if synchronous-p
(apply #'eshell/wait procs)
diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 0c381dbb86a..2b560afb92c 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -453,7 +453,7 @@ and the hook `eshell-exit-hook'."
last-command-event))))
(defun eshell-intercept-commands ()
- (when (and (eshell-interactive-process-p)
+ (when (and eshell-foreground-command
(not (and (integerp last-input-event)
(memq last-input-event '(?\C-x ?\C-c)))))
(let ((possible-events (where-is-internal this-command))
@@ -967,7 +967,7 @@ buffer's process if STRING contains a password prompt defined by
`eshell-password-prompt-regexp'.
This function could be in the list `eshell-output-filter-functions'."
- (when (eshell-interactive-process-p)
+ (when eshell-foreground-command
(save-excursion
(let ((case-fold-search t))
(goto-char eshell-last-output-block-begin)
diff --git a/lisp/eshell/eshell.el b/lisp/eshell/eshell.el
index a3f80f453eb..8765ba499a1 100644
--- a/lisp/eshell/eshell.el
+++ b/lisp/eshell/eshell.el
@@ -315,9 +315,8 @@ argument), then insert output into the current buffer at point."
;; make the output as attractive as possible, with no
;; extraneous newlines
(when intr
- (if (eshell-interactive-process-p)
- (eshell-wait-for-process (eshell-tail-process)))
- (cl-assert (not (eshell-interactive-process-p)))
+ (apply #'eshell-wait-for-process (cadr eshell-foreground-command))
+ (cl-assert (not eshell-foreground-command))
(goto-char (point-max))
(while (and (bolp) (not (bobp)))
(delete-char -1)))