scratch/eshell-lisp-pipe bc9d1ff27a9: Allow sending interactive input to Eshell workers

Jim Porter <[email protected]> Fri, 31 Jul 2026 13:45:40 -0400 (EDT)
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: scratch/eshell-lisp-pipe
commit bc9d1ff27a9070341e679b20a63c70d3e1ff055f
Author: Jim Porter <[email protected]>
Commit: Jim Porter <[email protected]>

    Allow sending interactive input to Eshell workers
    
    * lisp/eshell/esh-io.el (eshell-send-eof-to-target)
    (eshell-output-region-to-target): New generic methods.
    
    * lisp/eshell/esh-mode.el (eshell-send-input): Call
    'eshell-output-region-to-target'.
    (eshell-send-eof-to-process): Call 'eshell-send-eof-to-target'.
    
    * lisp/eshell/esh-worker.el (eshell-worker-initialize): New function.
    
    * test/lisp/eshell/esh-worker-tests.el
    (esh-worker-test/map-lines/interactive)
    (esh-worker-test/apply-lines/interactive): New tests.
---
 lisp/eshell/esh-io.el                | 24 ++++++++++++++++++++++++
 lisp/eshell/esh-mode.el              | 10 +++++-----
 lisp/eshell/esh-worker.el            |  6 ++++++
 test/lisp/eshell/esh-worker-tests.el | 31 +++++++++++++++++++++++++++++++
 4 files changed, 66 insertions(+), 5 deletions(-)

diff --git a/lisp/eshell/esh-io.el b/lisp/eshell/esh-io.el
index 395a641aed6..39e456ff9d3 100644
--- a/lisp/eshell/esh-io.el
+++ b/lisp/eshell/esh-io.el
@@ -702,6 +702,15 @@ If status is nil, prompt before killing."
   (when-let* ((close-function (eshell-function-target-close-function target)))
     (funcall close-function status)))
 
+(cl-defgeneric eshell-send-eof-to-target (target)
+  "Send end-of-file to TARGET.
+By default, this is equivalent to `eshell-close-target' with nil status."
+  (eshell-close-target target nil))
+
+(cl-defmethod eshell-send-eof-to-target ((target process))
+  "Send end-of-file to a TARGET process."
+  (process-send-eof target))
+
 (cl-defgeneric eshell-output-object-to-target (object target)
   "Output OBJECT to TARGET.
 Returns what was actually sent, or nil if nothing was sent.")
@@ -766,6 +775,21 @@ command output when `eshell-ensure-newline-p' is non-nil."
   "Return non-nil to indicate that the display is line-oriented."
   t)
 
+(cl-defgeneric eshell-output-region-to-target (start end target)
+  "Output the region from START to END to TARGET."
+  (eshell-output-object-to-target (buffer-substring start end) target))
+
+(cl-defmethod eshell-output-region-to-target (start end (target process))
+  "Output the region from START to END to the process TARGET."
+  (condition-case _
+      (process-send-region target start end)
+    (error
+     ;; NOTE: When running Emacs in batch mode (e.g. during regression
+     ;; tests), Emacs can abort due to SIGPIPE here.  Maybe
+     ;; `process-send-region' should handle SIGPIPE even in batch mode
+     ;; (bug#66186).
+     (signal 'eshell-pipe-broken (list target)))))
+
 (defun eshell-output-object (object &optional handle-index handles)
   "Insert OBJECT, using HANDLE-INDEX specifically.
 If HANDLE-INDEX is nil, output to `eshell-output-handle'.
diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el
index 3a6c9a2a7f4..057a1e91773 100644
--- a/lisp/eshell/esh-mode.el
+++ b/lisp/eshell/esh-mode.el
@@ -645,10 +645,10 @@ newline."
 	    (if (or eshell-send-direct-to-subprocesses
 		    (= eshell-last-input-start eshell-last-input-end))
 		(unless no-newline
-		  (process-send-string (eshell-head-process) "\n"))
-	      (process-send-region (eshell-head-process)
-				   eshell-last-input-start
-				   eshell-last-input-end)))
+		  (eshell-output-object-to-target "\n" (eshell-head-process)))
+	      (eshell-output-region-to-target eshell-last-input-start
+				              eshell-last-input-end
+                                              (eshell-head-process))))
 	(if (= eshell-last-output-end (point))
 	    (run-hooks 'eshell-post-command-hook)
 	  (let (input)
@@ -685,7 +685,7 @@ newline."
   (interactive)
   (eshell-send-input nil nil t)
   (when (eshell-head-process)
-    (process-send-eof (eshell-head-process))))
+    (eshell-send-eof-to-target (eshell-head-process))))
 
 (defsubst eshell-kill-new ()
   "Add the last input text to the kill ring."
diff --git a/lisp/eshell/esh-worker.el b/lisp/eshell/esh-worker.el
index cdbde95d41d..6f86ad02825 100644
--- a/lisp/eshell/esh-worker.el
+++ b/lisp/eshell/esh-worker.el
@@ -66,6 +66,12 @@ commands' output with ordinary Lisp."
   :tag "Worker support"
   :group 'eshell)
 
+(defun eshell-worker-initialize ()   ;Called from `eshell-mode' via intern-soft!
+  "Initialize the Eshell worker code."
+  (setq-local eshell-complex-commands
+              (append '("accumulate" "apply-lines" "map-lines")
+                      eshell-complex-commands)))
+
 (cl-defstruct (eshell-worker
                (:constructor eshell-worker-create)
                (:copier nil))
diff --git a/test/lisp/eshell/esh-worker-tests.el b/test/lisp/eshell/esh-worker-tests.el
index b9b1ea3c256..d51f5319a59 100644
--- a/test/lisp/eshell/esh-worker-tests.el
+++ b/test/lisp/eshell/esh-worker-tests.el
@@ -159,6 +159,21 @@ It should call the mapped function once per line."
      "{echo '10\n1'; echo '5\n20'} | map-lines #'1+"
      "\\`11\n16\n21\n\\'")))
 
+(ert-deftest esh-worker-test/map-lines/interactive ()
+  "Test that `map-lines' works interactively."
+  (with-temp-eshell
+    (let (output-begin)
+      (eshell-insert-command "map-lines #'upcase")
+      (insert "hello")
+      (setq output-begin (1+ (point)))  ; Add one to skip the next newline.
+      (eshell-send-input)
+      (should (string= (buffer-substring output-begin (point)) "HELLO\n"))
+      (insert "goodbye")
+      (setq output-begin (1+ (point)))
+      (eshell-send-input)
+      (should (string= (buffer-substring output-begin (point)) "GOODBYE\n"))
+      (eshell-send-eof-to-process))))
+
 (ert-deftest esh-worker-test/map-lines/error-handling ()
   "Test that `map-lines' catches errors."
   (with-temp-eshell
@@ -221,4 +236,20 @@ It should pass each line as an argument to the applied function."
              "\"hi\"\n\\'"))
     (should (= eshell-last-command-status 1))))
 
+(ert-deftest esh-worker-test/apply-lines/interactive ()
+  "Test that `apply-lines' works interactively."
+  (with-temp-eshell
+    (let (output-begin)
+      (eshell-insert-command "apply-lines #'+")
+      (insert "5")
+      (eshell-send-input)
+      (insert "8")
+      (eshell-send-input)
+      (insert "13")
+      (eshell-send-input)
+      (setq output-begin (point))
+      (eshell-send-eof-to-process)
+      (should (string= (buffer-substring output-begin (eshell-end-of-output))
+                       "26\n")))))
+
 ;;; esh-io-tests.el ends here