master 245bbdc0c5e 4/8: Add basic support in Eshell for piping to Lisp commands

Jim Porter <[email protected]>
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: master
commit 245bbdc0c5e3e38fe6330cc263056c0e7e5b63db
Author: Jim Porter <[email protected]>
Commit: Jim Porter <[email protected]>

    Add basic support in Eshell for piping to Lisp commands
    
    * lisp/eshell/esh-worker.el:
    * test/lisp/eshell/esh-worker-tests.el: New files.
    
    * lisp/eshell/esh-cmd.el (eshell-lisp-command): Call 'eshell-get-pipe'
    when the command is a pipe target.
    
    * lisp/eshell/em-basic.el (eshell/echo): Mark as producing literal
    results.
---
 lisp/eshell/em-basic.el              |   2 +
 lisp/eshell/esh-cmd.el               |  20 +++-
 lisp/eshell/esh-worker.el            | 194 +++++++++++++++++++++++++++++++++++
 test/lisp/eshell/esh-worker-tests.el |  84 +++++++++++++++
 4 files changed, 297 insertions(+), 3 deletions(-)

diff --git a/lisp/eshell/em-basic.el b/lisp/eshell/em-basic.el
index 54394614711..8147ae31afb 100644
--- a/lisp/eshell/em-basic.el
+++ b/lisp/eshell/em-basic.el
@@ -137,6 +137,8 @@ or `eshell-printn' for display."
         "To terminate with a newline, you should use -N instead."))
      (eshell-echo args output-newline))))
 
+(put 'eshell/echo 'eshell-literal-result t)
+
 (defun eshell/printnl (&rest args)
   "Print out each of the arguments as strings, separated by newlines."
   (let ((elems (flatten-tree args)))
diff --git a/lisp/eshell/esh-cmd.el b/lisp/eshell/esh-cmd.el
index 0282c00ae63..000c9329987 100644
--- a/lisp/eshell/esh-cmd.el
+++ b/lisp/eshell/esh-cmd.el
@@ -105,6 +105,7 @@
 (require 'esh-proc)
 (require 'esh-module)
 (require 'esh-ext)
+(require 'esh-worker)
 
 (require 'eldoc)
 (require 'generator)
@@ -1585,7 +1586,20 @@ a string naming a Lisp function."
     (let* ((eshell-ensure-newline-p t)
            (command-form-p (and (functionp object)
                                 (symbolp object)))
-           result)
+           (literal-result (when command-form-p
+                             (get object 'eshell-literal-result)))
+           result worker-result
+           (printer
+            (lambda (object)
+              (setq worker-result
+                    (cond
+                     ((and (not literal-result) (eshell-worker-p object))
+                      object)
+                     ((and (not literal-result)
+                           (memq eshell-in-pipeline-p '(t last))
+                           (eshell-get-pipe object)))
+                     (t
+                      (ignore (eshell-print-maybe-n object))))))))
       (if command-form-p
           (setq eshell-last-arguments (eshell-convert-args args object)
                 eshell-last-command-name (format "#<function %s>"
@@ -1593,7 +1607,7 @@ a string naming a Lisp function."
         (setq eshell-last-arguments args
               eshell-last-command-name "#<Lisp object>"))
       (setq result (eshell-exec-lisp
-                    #'eshell-print-maybe-n #'eshell-error-maybe-n
+                    printer #'eshell-error-maybe-n
                     object eshell-last-arguments (not command-form-p)))
       (when (memq eshell-in-pipeline-p '(nil last))
         (eshell-set-exit-info
@@ -1606,7 +1620,7 @@ a string naming a Lisp function."
                     (not result))
            2)
          result))
-      nil)))
+      worker-result)))
 
 (define-obsolete-function-alias 'eshell-lisp-command* #'eshell-lisp-command
   "31.1")
diff --git a/lisp/eshell/esh-worker.el b/lisp/eshell/esh-worker.el
new file mode 100644
index 00000000000..16233368af0
--- /dev/null
+++ b/lisp/eshell/esh-worker.el
@@ -0,0 +1,194 @@
+;;; esh-worker.el --- eshell workers  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Workers are an Emacs Lisp-based analogue to external processes,
+;; allowing for piping command output into them to perform some
+;; action.  For example, to uppercase the output of some other command:
+;;
+;;   echo hi | #'upcase
+;;
+;;;_* `accumulate' worker
+;;
+;; Eshell provides several ways to pipe output into a Lisp function.
+;; The simplest is the "accumulate" worker, which is the default
+;; behavior when piping directly to a Lisp function as above.  You can
+;; also express this explicitly, as in:
+;;
+;;   echo hi | accumulate #'upcase
+
+;;; Code:
+
+(require 'esh-io)
+
+(defgroup eshell-worker nil
+  "Eshell workers provide a way to construct process-like objects in Emacs
+Lisp that can serve as pipe targets, allowing you to manipulate another
+command's output with ordinary Lisp."
+  :tag "Worker support"
+  :group 'eshell)
+
+(cl-defstruct (eshell-worker
+               (:constructor eshell-worker-create)
+               (:copier nil))
+  "A structure representing an abstract process-like object.
+This can be inherited from to define the behavior of a running
+task in Eshell."
+  (status 'run)
+  (eshell-buffer (progn
+                   (cl-assert (derived-mode-p 'eshell-mode))
+                   (current-buffer))
+                 :read-only t)
+  (output-handles (eshell-duplicate-handles eshell-current-handles))
+  (ensure-newline-p t))
+
+(cl-defgeneric eshell-get-pipe (_object)
+  "Get the pipe associated with OBJECT, if any."
+  nil)
+
+(cl-defmethod eshell-get-pipe ((object eshell-worker)) object)
+(cl-defmethod eshell-get-pipe ((object buffer)) object)
+(cl-defmethod eshell-get-pipe ((object marker)) object)
+(cl-defmethod eshell-get-pipe ((object process)) object)
+
+(cl-defmethod eshell-get-pipe :extra "function" (object)
+  (if (functionp object)
+      (eshell/accumulate object)
+    (cl-call-next-method)))
+
+(cl-defmethod eshell-task-p ((_object eshell-worker))
+  t)
+
+(cl-defmethod eshell-task-status ((task eshell-worker))
+  (eshell-worker-status task))
+
+(cl-defmethod eshell-task-active-p ((task eshell-worker))
+  (eq (eshell-worker-status task) 'run))
+
+(cl-defmethod eshell-get-target ((raw-target eshell-worker) &optional _mode)
+  "Convert a raw `eshell-worker' RAW-TARGET into a valid output target.
+This just returns RAW-TARGET."
+  raw-target)
+
+(cl-defmethod eshell-output-object-to-target :around
+  (_object (target eshell-worker))
+  "Output OBJECT to the Eshell worker TARGET.
+This method is called around any primary method and is responsible
+for let-binding the proper value for `eshell-current-handles'."
+  (unless (eq (eshell-worker-status target) 'run)
+    (signal 'eshell-pipe-broken target))
+  (let ((eshell-current-handles (eshell-worker-output-handles target))
+        (eshell-ensure-newline-p (eshell-worker-ensure-newline-p target)))
+    (with-current-buffer (eshell-worker-eshell-buffer target)
+      (cl-call-next-method))
+    (setf (eshell-worker-ensure-newline-p target) eshell-ensure-newline-p)))
+
+(cl-defmethod eshell-close-target :around ((target eshell-worker) _status)
+  "Close the worker PROC, passing STATUS as the result.
+This method is called around any primary method and is responsible
+for let-binding the proper value for `eshell-current-handles',
+closing the handles when done, and calling
+`eshell-kill-process-function'."
+  (when (eq (eshell-worker-status target) 'run)
+    (let ((eshell-current-handles (eshell-worker-output-handles target))
+          (eshell-ensure-newline-p (eshell-worker-ensure-newline-p target)))
+      (with-current-buffer (eshell-worker-eshell-buffer target)
+        (cl-call-next-method)
+        (setf (eshell-worker-status target) 'exit)
+        (eshell-close-handles)
+        (declare-function eshell-kill-process-function "esh-proc" (proc status))
+        (eshell-kill-process-function target "finished\n")))))
+
+(defun eshell--apply-print (function args)
+  "Call FUNCTION with Eshell-converted ARGS and print the result."
+  (when-let* ((result (apply function (eshell-convert-args args function))))
+    (eshell-print-maybe-n result)
+    result))
+
+;; Accumulate worker
+
+(cl-defstruct (eshell-accumulate-worker
+               (:include eshell-worker)
+               (:constructor eshell-accumulate-worker-create))
+  "A worker that calls a Lisp function once with all output.
+When outputting data to this worker, it will accumulate all the text,
+calling a Lisp FUNCTION once at the end.
+
+When outputting non-string data types to this worker (e.g. lists), it
+will first convert the data to a string before concatenating it.  If
+this structure only receives a single non-string value as input, it will
+pass the value unaltered to FUNCTION."
+  (function nil :read-only t)
+  (buffer-or-value nil))
+
+(defsubst eshell-accumulate-worker--make-buffer (worker)
+  "Ensure WORKER has an internal buffer to add strings to."
+  (setf (eshell-accumulate-worker-buffer-or-value worker)
+        (generate-new-buffer " *eshell-worker*" t)))
+
+(cl-defmethod eshell-output-object-to-target
+  (object (target eshell-accumulate-worker))
+  "Send OBJECT to the accumulate-worker TARGET.
+This calls the function associated with the worker.
+
+The returned value is the OBJECT in the form that it was actually
+sent to TARGET (e.g. a string representing OBJECT)."
+  (let ((buf-or-val (eshell-accumulate-worker-buffer-or-value target)))
+    (cond
+     ((bufferp buf-or-val)
+      (setq object (eshell-stringify object t))
+      (with-current-buffer buf-or-val
+        (insert object)))
+     (buf-or-val
+      (cl-assert (listp buf-or-val))
+      (setq object (eshell-concat t (car buf-or-val) object))
+      (with-current-buffer (eshell-accumulate-worker--make-buffer target)
+        (insert object)))
+     ((stringp object)
+      (with-current-buffer (eshell-accumulate-worker--make-buffer target)
+        (insert object)))
+     (t
+      (setf (eshell-accumulate-worker-buffer-or-value target)
+            (list object))))
+    object))
+
+(cl-defmethod eshell-close-target ((target eshell-accumulate-worker) _status)
+  "Close the accumulate-worker TARGET, flushing its buffer."
+  (let* ((function (eshell-accumulate-worker-function target))
+         (buf-or-val (eshell-accumulate-worker-buffer-or-value target))
+         (input (if (bufferp buf-or-val)
+                    (with-current-buffer buf-or-val
+                      (eshell-mark-numeric-string (buffer-string)))
+                  (car buf-or-val))))
+    (eshell--apply-print function (list input))))
+
+(defun eshell/accumulate (function)
+  "Call FUNCTION once with all the accumulated Eshell output.
+When outputting data to this worker, it will accumulate all the text,
+calling FUNCTION once at the end.
+
+When outputting non-string data types to this worker (e.g. lists), it
+will first convert the data to a string before concatenating it.  If
+this structure only receives a single non-string value as input, it will
+pass the value unaltered to FUNCTION."
+  (eshell-accumulate-worker-create :function function))
+
+(provide 'esh-worker)
+;;; esh-worker.el ends here
diff --git a/test/lisp/eshell/esh-worker-tests.el b/test/lisp/eshell/esh-worker-tests.el
new file mode 100644
index 00000000000..831a3269480
--- /dev/null
+++ b/test/lisp/eshell/esh-worker-tests.el
@@ -0,0 +1,84 @@
+;;; esh-io-tests.el --- esh-io test suite  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+         (ert-resource-file "eshell-tests-helpers"))
+
+;;; Tests:
+
+(ert-deftest esh-worker-test/pipe/eshell-to-function ()
+  "Test that piping an Eshell command to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output "echo hi | #'upcase" "\\`HI\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/eshell-to-function/numeric ()
+  "Test that piping an Eshell command to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output "echo 42 | #'1+" "\\`43\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/external-to-function ()
+  "Test that piping an external command to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output "*echo hi | #'upcase" "\\`HI\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/multiple-to-function ()
+  "Test that piping multiple output batches to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output "{*echo hi; *echo bye} | #'upcase"
+                                 "\\`HI\nBYE\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/multiple-to-function/numeric ()
+  "Test that piping multiple numeric output batches to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output "{echo 1; echo 2} | #'1+"
+                                 "\\`13\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/eshell-to-lambda ()
+  "Test that piping an Eshell command to a lambda works."
+  (with-temp-eshell
+    (eshell-match-command-output "echo hi | (lambda (i) (concat \"> \" i))"
+                                 "\\`> hi\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/external-to-lambda ()
+  "Test that piping an external command to a lambda works."
+  (with-temp-eshell
+    (eshell-match-command-output "*echo hi | (lambda (i) (concat \"> \" i))"
+                                 "\\`> hi\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/multiple-to-lambda ()
+  "Test that piping multiple output batches to a lambda works."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "{*echo hi; *echo bye} | (lambda (i) (concat \"> \" i))"
+     "\\`> hi\nbye\n\\'")))
+
+(ert-deftest esh-worker-test/pipe/multiple-pipes ()
+  "Test that piping an Eshell command to a function works."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "echo hi | #'upcase | (lambda (i) (concat \"> \" i))"
+     "\\`> HI\n\\'")))
+
+;;; esh-io-tests.el ends here
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.