unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#66066: 30.0.50; [PATCH] Add support for more-complex Eshell commands in the background
@ 2023-09-18  4:45 Jim Porter
  2023-09-23 18:47 ` Jim Porter
  0 siblings, 1 reply; 3+ messages in thread
From: Jim Porter @ 2023-09-18  4:45 UTC (permalink / raw)
  To: 66066

[-- Attachment #1: Type: text/plain, Size: 539 bytes --]

Currently, only very simple Eshell commands can be run in the 
background. Anything more complicated, such as Eshell script files or 
things involving variable interpolation fail. For example, this should 
print "hi", but it doesn't print anything (aside from messages about the 
processes that started/stopped):

   *echo ${*echo hi} &

The attached patches fix this.

Note: In reality, this is just a preliminary set of changes to add full 
job control to Eshell, but that's more complex. (Hopefully, I'll get 
that working next though.)

[-- Attachment #2: 0001-Make-eshell-resume-eval-take-the-command-to-resume.patch --]
[-- Type: text/plain, Size: 2536 bytes --]

From c635e28b4b293968647ee1db8e05cf4db16fb879 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sun, 17 Sep 2023 13:56:51 -0700
Subject: [PATCH 1/2] ; Make 'eshell-resume-eval' take the command to resume

* lisp/eshell/esh-cmd.el (eshell-resume-eval): Take COMMAND to resume.
Update callers.
---
 lisp/eshell/esh-cmd.el | 31 ++++++++++++++-----------------
 1 file changed, 14 insertions(+), 17 deletions(-)

diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 0d73b2d6e69..9b0948c77ca 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -1003,7 +1003,8 @@ eshell-eval-command
     (setq eshell-current-command command)
     (let* (result
            (delim (catch 'eshell-incomplete
-                    (ignore (setq result (eshell-resume-eval))))))
+                    (ignore (setq result (eshell-resume-eval
+                                          eshell-current-command))))))
       (when delim
         (error "Unmatched delimiter: %S" delim))
       result)))
@@ -1019,24 +1020,20 @@ eshell-resume-command
              ;; that all of those processes are now dead.
              (member proc eshell-last-async-procs)
              (not (seq-some #'process-live-p eshell-last-async-procs)))
-    (eshell-resume-eval)))
+    (eshell-resume-eval eshell-current-command)))
 
-(defun eshell-resume-eval ()
-  "Destructively evaluate a form which may need to be deferred."
+(defun eshell-resume-eval (command)
+  "Destructively evaluate a COMMAND form which may need to be deferred."
   (setq eshell-last-async-procs nil)
-  (when eshell-current-command
-    (eshell-condition-case err
-        (let* (retval
-               (procs (catch 'eshell-defer
-                        (ignore
-                         (setq retval
-                               (eshell-do-eval
-                                eshell-current-command))))))
-          (if retval
-              (cadr retval)
-            (ignore (setq eshell-last-async-procs procs))))
-      (error
-       (error (error-message-string err))))))
+  (eshell-condition-case err
+      (let* (retval
+             (procs (catch 'eshell-defer
+                      (ignore (setq retval (eshell-do-eval command))))))
+        (if retval
+            (cadr retval)
+          (ignore (setq eshell-last-async-procs procs))))
+    (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."
-- 
2.25.1


[-- Attachment #3: 0002-Support-Eshell-iterative-evaluation-in-the-backgroun.patch --]
[-- Type: text/plain, Size: 11812 bytes --]

From 7ed112c39dd3424b2cb69732c7e27509b7c3da0d Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sun, 17 Sep 2023 20:17:59 -0700
Subject: [PATCH 2/2] Support Eshell iterative evaluation in the background

* lisp/eshell/esh-cmd.el (eshell-deferred-commands): New variable.
(eshell-parse-command): 'eshell-do-subjob' should be the outermost
call, if present.
(eshell-do-subjob): Call 'eshell-resume-eval' to split this command
off from its parent forms.
(eshell-resume-command): Consult 'eshell-deferred-commands'.
(eshell-resume-eval): New argument BACKGROUND.
(eshell-do-eval): Remove check for 'eshell-current-subjob-p'.  This is
handled differently now.

* lisp/eshell/esh-proc.el (eshell-subjob-messages): New variable...
(eshell-record-process-object, eshell-remove-process-entry):... use it.

* lisp/eshell/em-script.el (eshell-source-file): Disable subjob
messages.

* test/lisp/eshell/esh-cmd-tests.el
(esh-cmd-test/background/simple-command)
(esh-cmd-test/background/subcommand)
* test/lisp/eshell/em-script-tests.el
(em-script-test/source-script/background): New tests.
---
 lisp/eshell/em-script.el            |  7 ++-
 lisp/eshell/esh-cmd.el              | 67 ++++++++++++++++++-----------
 lisp/eshell/esh-proc.el             | 18 +++++---
 test/lisp/eshell/em-script-tests.el | 13 ++++++
 test/lisp/eshell/esh-cmd-tests.el   | 26 +++++++++++
 5 files changed, 98 insertions(+), 33 deletions(-)

diff --git a/lisp/eshell/em-script.el b/lisp/eshell/em-script.el
index 9f6f720b8b0..3a4c315ad15 100644
--- a/lisp/eshell/em-script.el
+++ b/lisp/eshell/em-script.el
@@ -94,7 +94,12 @@ eshell-source-file
       (setq cmd `(eshell-as-subcommand ,cmd)))
     (throw 'eshell-replace-command
            `(let ((eshell-command-name ',file)
-                  (eshell-command-arguments ',args))
+                  (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)
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 9b0948c77ca..b80e3f51d87 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -263,6 +263,7 @@ eshell-ensure-newline-p
 
 ;;; Internal Variables:
 
+(defvar-local eshell-deferred-commands nil)
 (defvar eshell-current-command nil)
 (defvar eshell-command-name nil)
 (defvar eshell-command-arguments nil)
@@ -418,8 +419,6 @@ eshell-parse-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
@@ -427,6 +426,8 @@ eshell-parse-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
@@ -750,10 +751,13 @@ eshell-separate-commands
 
 (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."
-  `(let ((eshell-current-subjob-p t))
-     ,object))
+We indicate that the process was run in the background by
+returning it as (:eshell-background . PROCS)."
+  `(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))
+     (eshell-resume-eval ',object 'background)))
 
 (defmacro eshell-commands (object &optional silent)
   "Place a valid set of handles, and context, around command OBJECT."
@@ -988,7 +992,7 @@ eshell-eval-command
 COMMAND, if any.  If COMMAND is a background command, return the
 process(es) in a cons cell like:
 
-  (:eshell-background . PROCESS)"
+  (:eshell-background . PROCS)"
   (if eshell-current-command
       ;; We can just stick the new command at the end of the current
       ;; one, and everything will happen as it should.
@@ -1010,28 +1014,42 @@ eshell-eval-command
       result)))
 
 (defun eshell-resume-command (proc status)
-  "Resume the current command when a pipeline ends."
-  (when (and proc
-             ;; Make sure STATUS is something we want to handle.
-             (stringp status)
-             (not (string= "stopped" status))
-             (not (string-match eshell-reset-signals status))
-             ;; 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 #'process-live-p eshell-last-async-procs)))
-    (eshell-resume-eval eshell-current-command)))
-
-(defun eshell-resume-eval (command)
-  "Destructively evaluate a COMMAND form which may need to be deferred."
-  (setq eshell-last-async-procs nil)
+  "Resume the current command when a pipeline ends.
+PROC is the process that invoked this from its sentinel, and
+STATUS is its status."
+  (when-let (proc
+             ((stringp status))
+             ((not (string= "stopped" status)))
+             ((not (string-match eshell-reset-signals status)))
+             ;; Find the deferred command associated with this process.
+             (deferred (assoc proc eshell-deferred-commands
+                              (lambda (i j) (memq j i))))
+             ;; Make sure that all of the processes in this pipeline
+             ;; are now dead.
+             ((not (seq-some #'process-live-p (car deferred)))))
+    ;; Remove this command from `eshell-deferred-commands'.  If it
+    ;; gets deferred again, `eshell-resume-eval' will re-add it.
+    (setq eshell-deferred-commands (delq deferred eshell-deferred-commands))
+    (apply #'eshell-resume-eval (cdr deferred))))
+
+(defun eshell-resume-eval (command &optional background)
+  "Destructively evaluate a COMMAND form which may need to be deferred.
+Return the result of COMMAND if it wasn't deferred.  If
+BACKGROUND is non-nil and Eshell defers COMMAND, return a list of
+the form (:eshell-background . PROCS)."
+  (unless background
+    (setq eshell-last-async-procs nil))
   (eshell-condition-case err
       (let* (retval
              (procs (catch 'eshell-defer
                       (ignore (setq retval (eshell-do-eval command))))))
         (if retval
             (cadr retval)
-          (ignore (setq eshell-last-async-procs procs))))
+          (push (list procs command background)
+                eshell-deferred-commands)
+          (if background
+              (cons :eshell-background procs)
+            (ignore (setq eshell-last-async-procs procs)))))
     (error
      (error (error-message-string err)))))
 
@@ -1241,13 +1259,12 @@ eshell-do-eval
 		    (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)
 		    (eshell-manipulate form "inserting ignore form"
 		      (setcar form 'ignore)
-		      (setcdr form nil))
+		      (setcdr form nul))
 		    (throw 'eshell-defer procs))
                 (list 'quote result))))))))))))
 
diff --git a/lisp/eshell/esh-proc.el b/lisp/eshell/esh-proc.el
index aed8f8af93d..2cb7f92516c 100644
--- a/lisp/eshell/esh-proc.el
+++ b/lisp/eshell/esh-proc.el
@@ -100,6 +100,8 @@ eshell-kill-hook
 (defvar eshell-supports-asynchronous-processes (fboundp 'make-process)
   "Non-nil if Eshell can create asynchronous processes.")
 
+(defvar eshell-subjob-messages t
+  "Non-nil if we should print process start/end messages for subjobs.")
 (defvar eshell-current-subjob-p nil)
 
 (defvar eshell-process-list nil
@@ -235,8 +237,9 @@ eshell-insert-process
 
 (defsubst eshell-record-process-object (object)
   "Record OBJECT as now running."
-  (when (and (eshell-processp object)
-	     eshell-current-subjob-p)
+  (when (and eshell-subjob-messages
+             eshell-current-subjob-p
+             (eshell-processp object))
     (require 'esh-mode)
     (declare-function eshell-interactive-print "esh-mode" (string))
     (eshell-interactive-print
@@ -245,11 +248,12 @@ eshell-record-process-object
 
 (defun eshell-remove-process-entry (entry)
   "Record the process ENTRY as fully completed."
-  (if (and (eshell-processp (car entry))
-	   (cdr entry)
-	   eshell-done-messages-in-minibuffer)
-      (message "[%s]+ Done %s" (process-name (car entry))
-	       (process-command (car entry))))
+  (when (and eshell-subjob-messages
+             eshell-done-messages-in-minibuffer
+             (eshell-processp (car entry))
+             (cdr entry))
+    (message "[%s]+ Done %s" (process-name (car entry))
+             (process-command (car entry))))
   (setq eshell-process-list
 	(delq entry eshell-process-list)))
 
diff --git a/test/lisp/eshell/em-script-tests.el b/test/lisp/eshell/em-script-tests.el
index 74328844778..02e4125d827 100644
--- a/test/lisp/eshell/em-script-tests.el
+++ b/test/lisp/eshell/em-script-tests.el
@@ -63,6 +63,19 @@ em-script-test/source-script/redirect/dev-null
         "\\`\\'"))
       (should (equal (buffer-string) "hibye")))))
 
+(ert-deftest em-script-test/source-script/background ()
+  "Test sourcing a script in the background."
+  (skip-unless (executable-find "echo"))
+  (ert-with-temp-file temp-file
+    :text "*echo hi\nif {[ foo = foo ]} {*echo bye}"
+    (eshell-with-temp-buffer bufname "old"
+      (with-temp-eshell
+       (eshell-match-command-output
+        (format "source %s > #<%s> &" temp-file bufname)
+        "\\`\\'")
+       (eshell-wait-for-subprocess t))
+      (should (equal (buffer-string) "hi\nbye\n")))))
+
 (ert-deftest em-script-test/source-script/arg-vars ()
   "Test sourcing script with $0, $1, ... variables."
   (ert-with-temp-file temp-file :text "printnl $0 \"$1 $2\""
diff --git a/test/lisp/eshell/esh-cmd-tests.el b/test/lisp/eshell/esh-cmd-tests.el
index 7c384471e93..0a973a89563 100644
--- a/test/lisp/eshell/esh-cmd-tests.el
+++ b/test/lisp/eshell/esh-cmd-tests.el
@@ -103,6 +103,32 @@ esh-cmd-test/let-rebinds-after-defer
             "}")
     "value\nexternal\nvalue\n")))
 
+\f
+;; Background command invocation
+
+(ert-deftest esh-cmd-test/background/simple-command ()
+  "Test invocation with a simple background command."
+  (skip-unless (executable-find "echo"))
+  (eshell-with-temp-buffer bufname ""
+    (with-temp-eshell
+     (eshell-match-command-output
+      (format "*echo hi > #<%s> &" bufname)
+      (rx "[echo" (? ".exe") "] " (+ digit) "\n"))
+     (eshell-wait-for-subprocess t))
+    (should (equal (buffer-string) "hi\n"))))
+
+(ert-deftest esh-cmd-test/background/subcommand ()
+  "Test invocation with a background command containing subcommands."
+  (skip-unless (and (executable-find "echo")
+                    (executable-find "rev")))
+  (eshell-with-temp-buffer bufname ""
+    (with-temp-eshell
+     (eshell-match-command-output
+      (format "*echo ${*echo hello | rev} > #<%s> &" bufname)
+      (rx "[echo" (? ".exe") "] " (+ digit) "\n"))
+     (eshell-wait-for-subprocess t))
+    (should (equal (buffer-string) "olleh\n"))))
+
 \f
 ;; Lisp forms
 
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* bug#66066: 30.0.50; [PATCH] Add support for more-complex Eshell commands in the background
  2023-09-18  4:45 bug#66066: 30.0.50; [PATCH] Add support for more-complex Eshell commands in the background Jim Porter
@ 2023-09-23 18:47 ` Jim Porter
  2023-10-03  3:57   ` Jim Porter
  0 siblings, 1 reply; 3+ messages in thread
From: Jim Porter @ 2023-09-23 18:47 UTC (permalink / raw)
  To: 66066

[-- Attachment #1: Type: text/plain, Size: 141 bytes --]

On 9/17/2023 9:45 PM, Jim Porter wrote:
> The attached patches fix this.

Attached are some new patches rebased onto my changes in bug#66164.

[-- Attachment #2: 0001-Don-t-print-subjob-messages-when-running-an-Eshell-s.patch --]
[-- Type: text/plain, Size: 4996 bytes --]

From f996f432f41ef89d5e419d33c81d4b3604243cce Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Fri, 22 Sep 2023 23:03:45 -0700
Subject: [PATCH 1/2] Don't print subjob messages when running an Eshell script
 in the background

* lisp/eshell/esh-proc.el (eshell-subjob-messages): New variable...
(eshell-record-process-object)
(eshell-remove-process-entry): ... check it.

* lisp/eshell/em-script.el (eshell-source-file): Set
'eshell-subjob-messages' to nil.

* lisp/eshell/esh-cmd.el (eshell-do-subjob): Set
'eshell-subjob-messages' to t.

* test/lisp/eshell/em-script-tests.el
(em-script-test/source-script/background): New test.
---
 lisp/eshell/em-script.el            |  7 ++++++-
 lisp/eshell/esh-cmd.el              |  5 ++++-
 lisp/eshell/esh-proc.el             | 18 +++++++++++-------
 test/lisp/eshell/em-script-tests.el | 13 +++++++++++++
 4 files changed, 34 insertions(+), 9 deletions(-)

diff --git a/lisp/eshell/em-script.el b/lisp/eshell/em-script.el
index 9f6f720b8b0..3a4c315ad15 100644
--- a/lisp/eshell/em-script.el
+++ b/lisp/eshell/em-script.el
@@ -94,7 +94,12 @@ eshell-source-file
       (setq cmd `(eshell-as-subcommand ,cmd)))
     (throw 'eshell-replace-command
            `(let ((eshell-command-name ',file)
-                  (eshell-command-arguments ',args))
+                  (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)
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 1d828bd7f82..fc7d54a758d 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -742,7 +742,10 @@ eshell-do-subjob
   "Evaluate a command OBJECT as a subjob.
 We indicate that the process was run in the background by returning it
 ensconced in a list."
-  `(let ((eshell-current-subjob-p t))
+  `(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))
 
 (defmacro eshell-commands (object &optional silent)
diff --git a/lisp/eshell/esh-proc.el b/lisp/eshell/esh-proc.el
index d15e1e7d09b..126c7d0f26e 100644
--- a/lisp/eshell/esh-proc.el
+++ b/lisp/eshell/esh-proc.el
@@ -100,6 +100,8 @@ eshell-kill-hook
 (defvar eshell-supports-asynchronous-processes (fboundp 'make-process)
   "Non-nil if Eshell can create asynchronous processes.")
 
+(defvar eshell-subjob-messages t
+  "Non-nil if we should print process start/end messages for subjobs.")
 (defvar eshell-current-subjob-p nil)
 
 (defvar eshell-process-list nil
@@ -243,8 +245,9 @@ eshell-insert-process
 
 (defsubst eshell-record-process-object (object)
   "Record OBJECT as now running."
-  (when (and (eshell-processp object)
-	     eshell-current-subjob-p)
+  (when (and eshell-subjob-messages
+             eshell-current-subjob-p
+             (eshell-processp object))
     (require 'esh-mode)
     (declare-function eshell-interactive-print "esh-mode" (string))
     (eshell-interactive-print
@@ -253,11 +256,12 @@ eshell-record-process-object
 
 (defun eshell-remove-process-entry (entry)
   "Record the process ENTRY as fully completed."
-  (if (and (eshell-processp (car entry))
-	   (cdr entry)
-	   eshell-done-messages-in-minibuffer)
-      (message "[%s]+ Done %s" (process-name (car entry))
-	       (process-command (car entry))))
+  (when (and eshell-subjob-messages
+             eshell-done-messages-in-minibuffer
+             (eshell-processp (car entry))
+             (cdr entry))
+    (message "[%s]+ Done %s" (process-name (car entry))
+             (process-command (car entry))))
   (setq eshell-process-list
 	(delq entry eshell-process-list)))
 
diff --git a/test/lisp/eshell/em-script-tests.el b/test/lisp/eshell/em-script-tests.el
index 74328844778..191755dcc3e 100644
--- a/test/lisp/eshell/em-script-tests.el
+++ b/test/lisp/eshell/em-script-tests.el
@@ -63,6 +63,19 @@ em-script-test/source-script/redirect/dev-null
         "\\`\\'"))
       (should (equal (buffer-string) "hibye")))))
 
+(ert-deftest em-script-test/source-script/background ()
+  "Test sourcing a script in the background."
+  (skip-unless (executable-find "echo"))
+  (ert-with-temp-file temp-file
+    :text "*echo hi"
+    (eshell-with-temp-buffer bufname "old"
+      (with-temp-eshell
+       (eshell-match-command-output
+        (format "source %s > #<%s> &" temp-file bufname)
+        "\\`\\'")
+       (eshell-wait-for-subprocess t))
+      (should (equal (buffer-string) "hi\n")))))
+
 (ert-deftest em-script-test/source-script/arg-vars ()
   "Test sourcing script with $0, $1, ... variables."
   (ert-with-temp-file temp-file :text "printnl $0 \"$1 $2\""
-- 
2.25.1


[-- Attachment #3: 0002-Support-Eshell-iterative-evaluation-in-the-backgroun.patch --]
[-- Type: text/plain, Size: 19049 bytes --]

From 61da4551d45af5f66675c69de4851b30688c6ee0 Mon Sep 17 00:00:00 2001
From: Jim Porter <jporterbugs@gmail.com>
Date: Sat, 23 Sep 2023 11:36:11 -0700
Subject: [PATCH 2/2] 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).

* 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, 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/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.

* doc/misc/eshell.texi (Bugs and ideas): Remove implemented feature.
---
 doc/misc/eshell.texi                |   2 -
 lisp/eshell/em-smart.el             |   2 +-
 lisp/eshell/esh-cmd.el              | 182 +++++++++++++++++++---------
 test/lisp/eshell/em-script-tests.el |   4 +-
 test/lisp/eshell/esh-cmd-tests.el   |  29 ++++-
 test/lisp/eshell/eshell-tests.el    |   2 +-
 6 files changed, 153 insertions(+), 68 deletions(-)

diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi
index 8b3eb72aa66..cc94f610615 100644
--- a/doc/misc/eshell.texi
+++ b/doc/misc/eshell.texi
@@ -2568,8 +2568,6 @@ Bugs and ideas
 @samp{$=[REGEXP]}.  It indexes into the directory ring.
 @end table
 
-@item Eshell scripts can't execute in the background
-
 @item Support zsh's ``Parameter Expansion'' syntax, i.e., @samp{$@{@var{name}:-@var{val}@}}
 
 @item Create a mode @code{eshell-browse}
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 @@ eshell-smart-display-move
        ((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..762e65cf639 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -263,7 +263,24 @@ eshell-ensure-newline-p
 
 ;;; 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 (COMMAND PROCESSES).  COMMAND 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 (COMMAND 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 @@ eshell-in-pipeline-p
 (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,29 @@ 'eshell-commands-forbidden
 
 (defsubst eshell-interactive-process-p ()
   "Return non-nil if there is a currently running command process."
-  eshell-last-async-procs)
+  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 +349,47 @@ eshell-complete-lisp-symbols
       (throw 'pcomplete-completions
 	     (all-completions pcomplete-stub obarray 'boundp)))))
 
+;; Deferred 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 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)
+  "Add COMMAND to our list of known commands.
+If non-nil, BACKGROUND indicates that this is a command running
+in the background."
+  (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 (COMMAND PROCESSES BACKGROUND),
+as with the arguments to `eshell-push-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 +460,6 @@ eshell-parse-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 +467,8 @@ eshell-parse-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 +793,13 @@ eshell-separate-commands
 
 (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 . PROCS)."
   `(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 +1033,12 @@ eshell-eval-command
 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 . PROCS)"
+  (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 +1047,66 @@ eshell-eval-command
                          (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))
+        ;; Remove this command from `eshell-foreground-command' or
+        ;; `eshell-background-commands'.  If it gets deferred again,
+        ;; `eshell-resume-eval' will re-add it.
+        ;; (eshell-pop-command cmd-info)
+        (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 if it wasn't deferred.  If
+BACKGROUND is non-nil and Eshell defers COMMAND, return a list of
+the form (:eshell-background . PROCS)."
+  (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))))))
+            ;; and when a signal or thrown value causes us to
+            ;; unwind.
+            (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 +1335,6 @@ eshell-do-eval
 		    (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/test/lisp/eshell/em-script-tests.el b/test/lisp/eshell/em-script-tests.el
index 191755dcc3e..02e4125d827 100644
--- a/test/lisp/eshell/em-script-tests.el
+++ b/test/lisp/eshell/em-script-tests.el
@@ -67,14 +67,14 @@ em-script-test/source-script/background
   "Test sourcing a script in the background."
   (skip-unless (executable-find "echo"))
   (ert-with-temp-file temp-file
-    :text "*echo hi"
+    :text "*echo hi\nif {[ foo = foo ]} {*echo bye}"
     (eshell-with-temp-buffer bufname "old"
       (with-temp-eshell
        (eshell-match-command-output
         (format "source %s > #<%s> &" temp-file bufname)
         "\\`\\'")
        (eshell-wait-for-subprocess t))
-      (should (equal (buffer-string) "hi\n")))))
+      (should (equal (buffer-string) "hi\nbye\n")))))
 
 (ert-deftest em-script-test/source-script/arg-vars ()
   "Test sourcing script with $0, $1, ... variables."
diff --git a/test/lisp/eshell/esh-cmd-tests.el b/test/lisp/eshell/esh-cmd-tests.el
index 643038f89ff..e0783b26ad6 100644
--- a/test/lisp/eshell/esh-cmd-tests.el
+++ b/test/lisp/eshell/esh-cmd-tests.el
@@ -103,6 +103,32 @@ esh-cmd-test/let-rebinds-after-defer
             "}")
     "value\nexternal\nvalue\n")))
 
+\f
+;; Background command invocation
+
+(ert-deftest esh-cmd-test/background/simple-command ()
+  "Test invocation with a simple background command."
+  (skip-unless (executable-find "echo"))
+  (eshell-with-temp-buffer bufname ""
+    (with-temp-eshell
+     (eshell-match-command-output
+      (format "*echo hi > #<%s> &" bufname)
+      (rx "[echo" (? ".exe") "] " (+ digit) "\n"))
+     (eshell-wait-for-subprocess t))
+    (should (equal (buffer-string) "hi\n"))))
+
+(ert-deftest esh-cmd-test/background/subcommand ()
+  "Test invocation with a background command containing subcommands."
+  (skip-unless (and (executable-find "echo")
+                    (executable-find "rev")))
+  (eshell-with-temp-buffer bufname ""
+    (with-temp-eshell
+     (eshell-match-command-output
+      (format "*echo ${*echo hello | rev} > #<%s> &" bufname)
+      (rx "[echo" (? ".exe") "] " (+ digit) "\n"))
+     (eshell-wait-for-subprocess t))
+    (should (equal (buffer-string) "olleh\n"))))
+
 \f
 ;; Lisp forms
 
@@ -453,8 +479,7 @@ esh-cmd-test/throw
                  "echo hi; (throw 'tag 42); echo bye"))
               42))
    (should (eshell-match-output "\\`hi\n\\'"))
-   (should-not eshell-current-command)
-   (should-not eshell-last-async-procs)
+   (should-not eshell-foreground-command)
    ;; Make sure we can call another command after throwing.
    (eshell-match-command-output "echo again" "\\`again\n")))
 
diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el
index 25c8cfd389c..5b2517c4ca5 100644
--- a/test/lisp/eshell/eshell-tests.el
+++ b/test/lisp/eshell/eshell-tests.el
@@ -132,7 +132,7 @@ eshell-test/queue-input
    (eshell-insert-command "sleep 1; echo slept")
    (eshell-insert-command "echo alpha" #'eshell-queue-input)
    (let ((start (marker-position (eshell-beginning-of-output))))
-     (eshell-wait-for (lambda () (not eshell-current-command)))
+     (eshell-wait-for (lambda () (not eshell-foreground-command)))
      (should (string-match "^slept\n.*echo alpha\nalpha\n$"
                            (buffer-substring-no-properties
                             start (eshell-end-of-output)))))))
-- 
2.25.1


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* bug#66066: 30.0.50; [PATCH] Add support for more-complex Eshell commands in the background
  2023-09-23 18:47 ` Jim Porter
@ 2023-10-03  3:57   ` Jim Porter
  0 siblings, 0 replies; 3+ messages in thread
From: Jim Porter @ 2023-10-03  3:57 UTC (permalink / raw)
  To: 66066-done

Version: 30.1

On 9/23/2023 11:47 AM, Jim Porter wrote:
> Attached are some new patches rebased onto my changes in bug#66164.

Merged to master as 498d31e9f05, and closing this now.





^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2023-10-03  3:57 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-09-18  4:45 bug#66066: 30.0.50; [PATCH] Add support for more-complex Eshell commands in the background Jim Porter
2023-09-23 18:47 ` Jim Porter
2023-10-03  3:57   ` Jim Porter

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).