Add ability for Eshell virtual targets to handle closing the target

This was documented to work by calling the output function with 'nil',
but that was never actually implemented.  Instead, for compatibility,
we now support a new (optional) close function.

* lisp/eshell/esh-io.el (eshell-virtual-targets): Update docstring.
(eshell-generic-target): New struct...
(eshell-function-target): ... inherit from it, and rename from
'eshell-virtual-target'.
(eshell-get-target): Handle already-created 'eshell-generic-target'.
(eshell-close-target): Call the target's close function if present.

* test/lisp/eshell/esh-io-tests.el (esh-io-test/virtual/device-close):
New test.

* doc/misc/eshell.texi (Redirection): Document the new behavior.
This commit is contained in:
Jim Porter 2024-05-23 14:52:07 -07:00
parent 4c924a5333
commit 9daf1085a9
3 changed files with 82 additions and 36 deletions

View file

@ -2392,18 +2392,35 @@ Adds the text passed to it to the clipboard.
@end table
@vindex eshell-virtual-targets
You can, of course, define your own virtual targets. They are defined
by adding a list of the form @samp{("/dev/name" @var{function}
@var{mode})} to @code{eshell-virtual-targets}. The first element is
the device name; @var{function} may be either a lambda or a function
name. If @var{mode} is @code{nil}, then the function is the output
function; if it is non-@code{nil}, then the function is passed the
redirection mode as a symbol--@code{overwrite} for @code{>},
@code{append} for @code{>>}, or @code{insert} for @code{>>>}--and the
function is expected to return the output function.
@vindex eshell-generic-target
@findex eshell-output-object-to-target
@findex eshell-close-target
You can, of course, define your own virtual targets. These are entries
in @code{eshell-virtual-targets} with the form @samp{(@var{filename}
@var{output-function} @var{pass-mode})}. The first element,
@var{filename}, is the device name, usually of the form
@samp{"/dev/@var{name}"}. The second, @var{output-function}, should be a
function: Eshell will repeatedly call it with the redirected output.
This argument can also be an @code{eshell-generic-target} instance. In
this case, Eshell will repeatedly call the generic function
@code{eshell-output-object-to-target} with the output; once the
redirection has completed, Eshell will then call the generic function
@code{eshell-close-target}, passing non-@code{nil} if the redirected
command succeeded.
The output function is called once on each line of output until
@code{nil} is passed, indicating end of output.
If @var{pass-mode} is non-@code{nil}, then Eshell will pass the
redirection mode as an argument to @code{output-function} as a
symbol: @code{overwrite} for @code{>}, @code{append} for @code{>>}, or
@code{insert} for @code{>>>}. In this case, @code{output-function}
should return the real output function (either an ordinary function or
an @code{eshell-generic-target} as described above).
@defun eshell-function-target-create output-function &optional close-function
Create a new virtual target for Eshell that repeatedly calls
@var{output-function} with the redirected output, as described above.
If @var{close-function} is non-nil, Eshell will call it when closing the
target, passing non-@code{nil} if the redirected command succeeded.
@end defun
@node Pipelines
@section Pipelines

View file

@ -135,18 +135,22 @@ from executing while Emacs is redisplaying."
#'eshell-clipboard-append)
t))
"Map virtual devices name to Emacs Lisp functions.
If the user specifies any of the filenames above as a redirection
target, the function in the second element will be called.
Each member is of the following form:
If the third element is non-nil, the redirection mode is passed as an
argument (which is the symbol `overwrite', `append' or `insert'), and
the function is expected to return another function -- which is the
output function. Otherwise, the second element itself is the output
function.
(FILENAME OUTPUT-FUNCTION [PASS-MODE])
The output function is then called repeatedly with single strings,
which represents successive pieces of the output of the command, until nil
is passed, meaning EOF."
When the user specifies FILENAME as a redirection target, Eshell will
repeatedly call the OUTPUT-FUNCTION with the redirected output as
strings. OUTPUT-FUNCTION can also be an `eshell-generic-target'
instance. In this case, Eshell will repeatedly call the function in the
`output-function' slot with the string output; once the redirection has
completed, Eshell will then call the function in the `close-function'
slot, passing the exit status of the redirected command.
If PASS-MODE is non-nil, Eshell will pass the redirection mode as an
argument (which is the symbol `overwrite', `append' or `insert') to
OUTPUT-FUNCTION, which should return the real output function (either an
ordinary function or `eshell-generic-target' as desribed above)."
:version "30.1"
:type '(repeat
(list (string :tag "Target")
@ -499,11 +503,18 @@ after all printing is over with no argument."
(eshell-print object)
(eshell-print "\n"))
(cl-defstruct (eshell-virtual-target
(cl-defstruct (eshell-generic-target (:constructor nil))
"An Eshell target.
This is mainly useful for creating virtual targets (see
`eshell-virtual-targets').")
(cl-defstruct (eshell-function-target
(:include eshell-generic-target)
(:constructor nil)
(:constructor eshell-virtual-target-create (output-function)))
"A virtual target (see `eshell-virtual-targets')."
output-function)
(:constructor eshell-function-target-create
(output-function &optional close-function)))
"An Eshell target that calls an OUTPUT-FUNCTION."
output-function close-function)
(cl-defgeneric eshell-get-target (raw-target &optional _mode)
"Convert RAW-TARGET, which is a raw argument, into a valid output target.
@ -514,14 +525,16 @@ it defaults to `insert'."
(cl-defmethod eshell-get-target ((raw-target string) &optional mode)
"Convert a string RAW-TARGET into a valid output target using MODE.
If TARGET is a virtual target (see `eshell-virtual-targets'),
return an `eshell-virtual-target' instance; otherwise, return a
return an `eshell-generic-target' instance; otherwise, return a
marker for a file named TARGET."
(setq mode (or mode 'insert))
(if-let ((redir (assoc raw-target eshell-virtual-targets)))
(eshell-virtual-target-create
(if (nth 2 redir)
(funcall (nth 1 redir) mode)
(nth 1 redir)))
(let ((target (if (nth 2 redir)
(funcall (nth 1 redir) mode)
(nth 1 redir))))
(unless (eshell-generic-target-p target)
(setq target (eshell-function-target-create target)))
target)
(let ((exists (get-file-buffer raw-target))
(buf (find-file-noselect raw-target t)))
(with-current-buffer buf
@ -602,9 +615,10 @@ If status is nil, prompt before killing."
(throw 'done nil))
(process-send-eof target))))
(cl-defmethod eshell-close-target ((_target eshell-virtual-target) _status)
"Close a virtual TARGET."
nil)
(cl-defmethod eshell-close-target ((target eshell-function-target) status)
"Close an Eshell function TARGET."
(when-let ((close-function (eshell-function-target-close-function target)))
(funcall close-function status)))
(cl-defgeneric eshell-output-object-to-target (object target)
"Output OBJECT to TARGET.
@ -660,9 +674,9 @@ Returns what was actually sent, or nil if nothing was sent.")
object)
(cl-defmethod eshell-output-object-to-target (object
(target eshell-virtual-target))
"Output OBJECT to the virtual TARGET."
(funcall (eshell-virtual-target-output-function target) object))
(target eshell-function-target))
"Output OBJECT to the Eshell function TARGET."
(funcall (eshell-function-target-output-function target) object))
(defun eshell-output-object (object &optional handle-index handles)
"Insert OBJECT, using HANDLE-INDEX specifically.

View file

@ -381,4 +381,19 @@ stdout originally pointed (the terminal)."
(eshell-insert-command "echo three >> /dev/kill")
(should (equal (car kill-ring) "twothree"))))
(ert-deftest esh-io-test/virtual/device-close ()
"Check that the close function for `eshell-function-target' works."
(let* ((data nil)
(status nil)
(eshell-virtual-targets
`(("/dev/virtual"
,(eshell-function-target-create
(lambda (d) (setq data d))
(lambda (s) (setq status s)))
nil))))
(with-temp-eshell
(eshell-insert-command "echo hello > /dev/virtual")
(should (equal data "hello"))
(should (equal status t)))))
;;; esh-io-tests.el ends here