mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-06-14 12:31:25 +00:00
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:
parent
be067246af
commit
751dccb17e
4 changed files with 294 additions and 3 deletions
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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
191
lisp/eshell/esh-worker.el
Normal 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
|
||||
84
test/lisp/eshell/esh-worker-tests.el
Normal file
84
test/lisp/eshell/esh-worker-tests.el
Normal 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
|
||||
Loading…
Reference in a new issue