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.
This commit is contained in:
Jim Porter 2026-05-18 17:18:26 -07:00
parent be067246af
commit 751dccb17e
4 changed files with 294 additions and 3 deletions

View file

@ -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)))

View file

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

191
lisp/eshell/esh-worker.el Normal file
View file

@ -0,0 +1,191 @@
;;; 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 manipulating other
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)
(with-current-buffer buf-or-val
(insert (eshell-stringify object t))))
(buf-or-val
(cl-assert (listp buf-or-val))
(with-current-buffer (eshell-accumulate-worker--make-buffer target)
(insert (eshell-concat t (car buf-or-val) 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))))))
(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

View file

@ -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