1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-04-27 16:51:06 -07:00

Simplify Eshell handle functions and add tests/documentation

* lisp/eshell/esh-arg.el (eshell-parse-argument-hook): Explain how to
use 'eshell-finish-arg'.

* lisp/eshell/esh-io.el (eshell-create-handles): Only call
'eshell-get-target' for stderr if necessary.
(eshell-protect-handles): Use 'dotimes'.
(eshell-set-output-handle): Pass HANDLES and fix an edge case with
setting a duplicate TARGET.

* test/lisp/eshell/eshell-tests-helpers.el (eshell-with-temp-buffer):
New macro.

* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/quoted-lisp-form)
(esh-cmd-test/backquoted-lisp-form)
(esh-cmd-test/backquoted-lisp-form/splice): New tests.

* test/lisp/eshell/eshell-tests.el (eshell-test/redirect-buffer)
(eshell-test/redirect-buffer-escaped): Move to...
* test/lisp/eshell/esh-io-tests.el: ... here, and add other I/O tests.

* doc/misc/eshell.texi (Arguments): Add documentation for special
argument types.
(Input/Output): Expand documentation for redirection and pipelines.
This commit is contained in:
Jim Porter 2022-07-09 10:34:31 -07:00
parent 5af5ed6c62
commit 1be925faa1
7 changed files with 411 additions and 72 deletions

View file

@ -256,7 +256,6 @@ as an argument will ``spread'' the elements into multiple arguments:
@end example
@subsection Quoting and escaping
As with other shells, you can escape special characters and spaces
with by prefixing the character with a backslash (@code{\}), or by
surrounding the string with apostrophes (@code{''}) or double quotes
@ -268,6 +267,40 @@ When using expansions (@pxref{Expansion}) in an Eshell command, the
result may potentially be of any data type. To ensure that the result
is always a string, the expansion can be surrounded by double quotes.
@subsection Special argument types
In addition to strings and numbers, Eshell supports a number of
special argument types. These let you refer to various other Emacs
Lisp data types, such as lists or buffers.
@table @code
@item #'@var{lisp-form}
This refers to the quoted Emacs Lisp form @var{lisp-form}. Though
this looks similar to the ``sharp quote'' syntax for functions
(@pxref{Special Read Syntax, , , elisp, The Emacs Lisp Reference
Manual}), it instead corresponds to @code{quote} and can be used for
any quoted form.@footnote{Eshell would interpret a bare apostrophe
(@code{'}) as the start of a single-quoted string.}
@item `@var{lisp-form}
This refers to the backquoted Emacs Lisp form @var{lisp-form}
(@pxref{Backquote, , , elisp, The Emacs Lisp Reference Manual}). As
in Emacs Lisp, you can use @samp{,} and @samp{,@@} to refer to
non-constant values.
@item #<buffer @var{name}>
@itemx #<@var{name}>
Return the buffer named @var{name}. This is equivalent to
@samp{$(get-buffer-create "@var{name}")} (@pxref{Creating Buffers, , ,
elisp, The Emacs Lisp Reference Manual}).
@item #<process @var{name}>
Return the process named @var{name}. This is equivalent to
@samp{$(get-process "@var{name}")} (@pxref{Process Information, , ,
elisp, The Emacs Lisp Reference Manual}).
@end table
@node Built-ins
@section Built-in commands
Several commands are built-in in Eshell. In order to call the
@ -1560,6 +1593,13 @@ Reverses the order of a list of values.
Since Eshell does not communicate with a terminal like most command
shells, IO is a little different.
@menu
* Visual Commands::
* Redirection::
* Pipelines::
@end menu
@node Visual Commands
@section Visual Commands
If you try to run programs from within Eshell that are not
line-oriented, such as programs that use ncurses, you will just get
@ -1592,40 +1632,104 @@ program exits, customize the variable
@code{eshell-destroy-buffer-when-process-dies} to a non-@code{nil}
value; the default is @code{nil}.
@node Redirection
@section Redirection
Redirection is mostly the same in Eshell as it is in other command
shells. The output redirection operators @code{>} and @code{>>} as
well as pipes are supported, but there is not yet any support for
input redirection. Output can also be redirected to buffers, using
the @code{>>>} redirection operator, and Elisp functions, using
virtual devices.
Redirection in Eshell is similar to that of other command shells. You
can use the output redirection operators @code{>} and @code{>>}, but
there is not yet any support for input redirection. In the cases
below, @var{fd} specifies the file descriptor to redirect; if not
specified, file descriptor 1 (standard output) will be used by
default.
The buffer redirection operator, @code{>>>}, expects a buffer object
on the right-hand side, into which it inserts the output of the
left-hand side. e.g., @samp{echo hello >>> #<buffer *scratch*>}
inserts the string @code{"hello"} into the @file{*scratch*} buffer.
The convenience shorthand variant @samp{#<@var{buffer-name}>}, as in
@samp{#<*scratch*>}, is also accepted.
@table @code
@code{eshell-virtual-targets} is a list of mappings of virtual device
names to functions. Eshell comes with two virtual devices:
@file{/dev/kill}, which sends the text to the kill ring, and
@file{/dev/clip}, which sends text to the clipboard.
@item > @var{dest}
@itemx @var{fd}> @var{dest}
Redirect output to @var{dest}, overwriting its contents with the new
output.
@item >> @var{dest}
@itemx @var{fd}>> @var{dest}
Redirect output to @var{dest}, appending it to the existing contents
of @var{dest}.
@item >>> @var{buffer}
@itemx @var{fd}>>> @var{buffer}
Redirect output to @var{dest}, inserting it at the current mark if
@var{dest} is a buffer, at the beginning of the file if @var{dest} is
a file, or otherwise behaving the same as @code{>>}.
@end table
Eshell supports redirecting output to several different types of
targets:
@itemize @bullet
@item
files, including virtual targets (see below);
@item
buffers (@pxref{Buffers, , , elisp, GNU Emacs Lisp Reference Manual});
@item
markers (@pxref{Markers, , , elisp, GNU Emacs Lisp Reference Manual});
@item
processes (@pxref{Processes, , , elisp, GNU Emacs Lisp Reference
Manual}); and
@item
symbols (@pxref{Symbols, , , elisp, GNU Emacs Lisp Reference Manual}).
@end itemize
@subsection Virtual Targets
Virtual targets are mapping of device names to functions. Eshell
comes with four virtual devices:
@table @file
@item /dev/null
Does nothing with the output passed to it.
@item /dev/eshell
Writes the text passed to it to the display.
@item /dev/kill
Adds the text passed to it to the kill ring.
@item /dev/clip
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.
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.
The output function is called once on each line of output until
@code{nil} is passed, indicating end of output.
@section Running Shell Pipelines Natively
@node Pipelines
@section Pipelines
As with most other shells, Eshell supports pipelines to pass the
output of one command the input of the next command. You can pipe
commands to each other using the @code{|} operator. For example,
@example
~ $ echo hello | rev
olleh
@end example
@subsection Running Shell Pipelines Natively
When constructing shell pipelines that will move a lot of data, it is
a good idea to bypass Eshell's own pipelining support and use the
operating system shell's instead. This is especially relevant when

View file

@ -147,6 +147,10 @@ return the result of the parse as a sexp. It is also responsible for
moving the point forward to reflect the amount of input text that was
parsed.
If the hook determines that it has reached the end of an argument, it
should call `eshell-finish-arg' to complete processing of the current
argument and proceed to the next.
If no function handles the current character at point, it will be
treated as a literal character."
:type 'hook

View file

@ -236,22 +236,21 @@ The default location for standard output and standard error will go to
STDOUT and STDERR, respectively.
OUTPUT-MODE and ERROR-MODE are either `overwrite', `append' or `insert';
a nil value of mode defaults to `insert'."
(let ((handles (make-vector eshell-number-of-handles nil))
(output-target (eshell-get-target stdout output-mode))
(error-target (eshell-get-target stderr error-mode)))
(let* ((handles (make-vector eshell-number-of-handles nil))
(output-target (eshell-get-target stdout output-mode))
(error-target (if stderr
(eshell-get-target stderr error-mode)
output-target)))
(aset handles eshell-output-handle (cons output-target 1))
(aset handles eshell-error-handle
(cons (if stderr error-target output-target) 1))
(aset handles eshell-error-handle (cons error-target 1))
handles))
(defun eshell-protect-handles (handles)
"Protect the handles in HANDLES from a being closed."
(let ((idx 0))
(while (< idx eshell-number-of-handles)
(if (aref handles idx)
(setcdr (aref handles idx)
(1+ (cdr (aref handles idx)))))
(setq idx (1+ idx))))
(dotimes (idx eshell-number-of-handles)
(when (aref handles idx)
(setcdr (aref handles idx)
(1+ (cdr (aref handles idx))))))
handles)
(defun eshell-close-handles (&optional exit-code result handles)
@ -278,6 +277,24 @@ the value already set in `eshell-last-command-result'."
(eshell-close-target target (= eshell-last-command-status 0)))
(setcar handle nil))))))
(defun eshell-set-output-handle (index mode &optional target handles)
"Set handle INDEX for the current HANDLES to point to TARGET using MODE.
If HANDLES is nil, use `eshell-current-handles'."
(when target
(let ((handles (or handles eshell-current-handles)))
(if (and (stringp target)
(string= target (null-device)))
(aset handles index nil)
(let ((where (eshell-get-target target mode))
(current (car (aref handles index))))
(if (listp current)
(unless (member where current)
(setq current (append current (list where))))
(setq current (list where)))
(if (not (aref handles index))
(aset handles index (cons nil 1)))
(setcar (aref handles index) current))))))
(defun eshell-close-target (target status)
"Close an output TARGET, passing STATUS as the result.
STATUS should be non-nil on successful termination of the output."
@ -390,22 +407,6 @@ it defaults to `insert'."
(error "Invalid redirection target: %s"
(eshell-stringify target)))))
(defun eshell-set-output-handle (index mode &optional target)
"Set handle INDEX, using MODE, to point to TARGET."
(when target
(if (and (stringp target)
(string= target (null-device)))
(aset eshell-current-handles index nil)
(let ((where (eshell-get-target target mode))
(current (car (aref eshell-current-handles index))))
(if (and (listp current)
(not (member where current)))
(setq current (append current (list where)))
(setq current (list where)))
(if (not (aref eshell-current-handles index))
(aset eshell-current-handles index (cons nil 1)))
(setcar (aref eshell-current-handles index) current)))))
(defun eshell-interactive-output-p ()
"Return non-nil if current handles are bound for interactive display."
(and (eq (car (aref eshell-current-handles

View file

@ -73,6 +73,25 @@ Test that trailing arguments outside the subcommand are ignored.
e.g. \"{(+ 1 2)} 3\" => 3"
(eshell-command-result-equal "{(+ 1 2)} 3" 3))
;; Lisp forms
(ert-deftest esh-cmd-test/quoted-lisp-form ()
"Test parsing of a quoted Lisp form."
(eshell-command-result-equal "echo #'(1 2)" '(1 2)))
(ert-deftest esh-cmd-test/backquoted-lisp-form ()
"Test parsing of a backquoted Lisp form."
(let ((eshell-test-value 42))
(eshell-command-result-equal "echo `(answer ,eshell-test-value)"
'(answer 42))))
(ert-deftest esh-cmd-test/backquoted-lisp-form/splice ()
"Test parsing of a backquoted Lisp form using splicing."
(let ((eshell-test-value '(2 3)))
(eshell-command-result-equal "echo `(1 ,@eshell-test-value)"
'(1 2 3))))
;; Logical operators

View file

@ -0,0 +1,220 @@
;;; esh-io-tests.el --- esh-io test suite -*- lexical-binding:t -*-
;; Copyright (C) 2022 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 'esh-mode)
(require 'eshell)
(require 'eshell-tests-helpers
(expand-file-name "eshell-tests-helpers"
(file-name-directory (or load-file-name
default-directory))))
(defvar eshell-test-value nil)
(defun eshell-test-file-string (file)
"Return the contents of FILE as a string."
(with-temp-buffer
(insert-file-contents file)
(buffer-string)))
(defun eshell/test-output ()
"Write some test output separately to stdout and stderr."
(eshell-printn "stdout")
(eshell-errorn "stderr"))
;;; Tests:
;; Basic redirection
(ert-deftest esh-io-test/redirect-file/overwrite ()
"Check that redirecting to a file in overwrite mode works."
(ert-with-temp-file temp-file
:text "old"
(with-temp-eshell
(eshell-insert-command (format "echo new > %s" temp-file)))
(should (equal (eshell-test-file-string temp-file) "new"))))
(ert-deftest esh-io-test/redirect-file/append ()
"Check that redirecting to a file in append mode works."
(ert-with-temp-file temp-file
:text "old"
(with-temp-eshell
(eshell-insert-command (format "echo new >> %s" temp-file)))
(should (equal (eshell-test-file-string temp-file) "oldnew"))))
(ert-deftest esh-io-test/redirect-file/insert ()
"Check that redirecting to a file in insert works."
(ert-with-temp-file temp-file
:text "old"
(with-temp-eshell
(eshell-insert-command (format "echo new >>> %s" temp-file)))
(should (equal (eshell-test-file-string temp-file) "newold"))))
(ert-deftest esh-io-test/redirect-buffer/overwrite ()
"Check that redirecting to a buffer in overwrite mode works."
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-insert-command (format "echo new > #<%s>" bufname)))
(should (equal (buffer-string) "new"))))
(ert-deftest esh-io-test/redirect-buffer/append ()
"Check that redirecting to a buffer in append mode works."
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-insert-command (format "echo new >> #<%s>" bufname)))
(should (equal (buffer-string) "oldnew"))))
(ert-deftest esh-io-test/redirect-buffer/insert ()
"Check that redirecting to a buffer in insert mode works."
(eshell-with-temp-buffer bufname "old"
(goto-char (point-min))
(with-temp-eshell
(eshell-insert-command (format "echo new >>> #<%s>" bufname)))
(should (equal (buffer-string) "newold"))))
(ert-deftest esh-io-test/redirect-buffer/escaped ()
"Check that redirecting to a buffer with escaped characters works."
(with-temp-buffer
(rename-buffer "eshell\\temp\\buffer" t)
(let ((bufname (buffer-name)))
(with-temp-eshell
(eshell-insert-command (format "echo hi > #<%s>"
(string-replace "\\" "\\\\" bufname))))
(should (equal (buffer-string) "hi")))))
(ert-deftest esh-io-test/redirect-symbol/overwrite ()
"Check that redirecting to a symbol in overwrite mode works."
(let ((eshell-test-value "old"))
(with-temp-eshell
(eshell-insert-command "echo new > #'eshell-test-value"))
(should (equal eshell-test-value "new"))))
(ert-deftest esh-io-test/redirect-symbol/append ()
"Check that redirecting to a symbol in append mode works."
(let ((eshell-test-value "old"))
(with-temp-eshell
(eshell-insert-command "echo new >> #'eshell-test-value"))
(should (equal eshell-test-value "oldnew"))))
(ert-deftest esh-io-test/redirect-marker ()
"Check that redirecting to a marker works."
(with-temp-buffer
(let ((eshell-test-value (point-marker)))
(with-temp-eshell
(eshell-insert-command "echo hi > $eshell-test-value"))
(should (equal (buffer-string) "hi")))))
(ert-deftest esh-io-test/redirect-multiple ()
"Check that redirecting to multiple targets works."
(let ((eshell-test-value "old"))
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-insert-command (format "echo new > #<%s> > #'eshell-test-value"
bufname)))
(should (equal (buffer-string) "new"))
(should (equal eshell-test-value "new")))))
(ert-deftest esh-io-test/redirect-multiple/repeat ()
"Check that redirecting to multiple targets works when repeating a target."
(let ((eshell-test-value "old"))
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-insert-command
(format "echo new > #<%s> > #'eshell-test-value > #<%s>"
bufname bufname)))
(should (equal (buffer-string) "new"))
(should (equal eshell-test-value "new")))))
;; Redirecting specific handles
(ert-deftest esh-io-test/redirect-stdout ()
"Check that redirecting to stdout doesn't redirect stderr."
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-match-command-output (format "test-output > #<%s>" bufname)
"stderr\n"))
(should (equal (buffer-string) "stdout\n")))
;; Also check explicitly specifying the stdout fd.
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-match-command-output (format "test-output 1> #<%s>" bufname)
"stderr\n"))
(should (equal (buffer-string) "stdout\n"))))
(ert-deftest esh-io-test/redirect-stderr/overwrite ()
"Check that redirecting to stderr doesn't redirect stdout."
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-match-command-output (format "test-output 2> #<%s>" bufname)
"stdout\n"))
(should (equal (buffer-string) "stderr\n"))))
(ert-deftest esh-io-test/redirect-stderr/append ()
"Check that redirecting to stderr doesn't redirect stdout."
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-match-command-output (format "test-output 2>> #<%s>" bufname)
"stdout\n"))
(should (equal (buffer-string) "oldstderr\n"))))
(ert-deftest esh-io-test/redirect-stderr/insert ()
"Check that redirecting to stderr doesn't redirect stdout."
(eshell-with-temp-buffer bufname "old"
(goto-char (point-min))
(with-temp-eshell
(eshell-match-command-output (format "test-output 2>>> #<%s>" bufname)
"stdout\n"))
(should (equal (buffer-string) "stderr\nold"))))
(ert-deftest esh-io-test/redirect-stdout-and-stderr ()
"Check that redirecting to both stdout and stderr works."
(eshell-with-temp-buffer bufname-1 "old"
(eshell-with-temp-buffer bufname-2 "old"
(with-temp-eshell
(eshell-match-command-output (format "test-output > #<%s> 2> #<%s>"
bufname-1 bufname-2)
"\\`\\'"))
(should (equal (buffer-string) "stderr\n")))
(should (equal (buffer-string) "stdout\n"))))
;; Virtual targets
(ert-deftest esh-io-test/virtual-dev-eshell ()
"Check that redirecting to /dev/eshell works."
(with-temp-eshell
(eshell-match-command-output "echo hi > /dev/eshell" "hi")))
(ert-deftest esh-io-test/virtual-dev-kill ()
"Check that redirecting to /dev/kill works."
(with-temp-eshell
(eshell-insert-command "echo one > /dev/kill")
(should (equal (car kill-ring) "one"))
(eshell-insert-command "echo two > /dev/kill")
(should (equal (car kill-ring) "two"))
(eshell-insert-command "echo three >> /dev/kill")
(should (equal (car kill-ring) "twothree"))))
;;; esh-io-tests.el ends here

View file

@ -51,6 +51,16 @@ See `eshell-wait-for-subprocess'.")
(let (kill-buffer-query-functions)
(kill-buffer eshell-buffer)))))))
(defmacro eshell-with-temp-buffer (bufname text &rest body)
"Create a temporary buffer containing TEXT and evaluate BODY there.
BUFNAME will be set to the name of the temporary buffer."
(declare (indent 2))
`(with-temp-buffer
(insert ,text)
(rename-buffer "eshell-temp-buffer" t)
(let ((,bufname (buffer-name)))
,@body)))
(defun eshell-wait-for-subprocess (&optional all)
"Wait until there is no interactive subprocess running in Eshell.
If ALL is non-nil, wait until there are no Eshell subprocesses at

View file

@ -105,25 +105,6 @@
(format template "format \"%s\" eshell-in-pipeline-p")
"nil")))
(ert-deftest eshell-test/redirect-buffer ()
"Check that piping to a buffer works"
(with-temp-buffer
(rename-buffer "eshell-temp-buffer" t)
(let ((bufname (buffer-name)))
(with-temp-eshell
(eshell-insert-command (format "echo hi > #<%s>" bufname)))
(should (equal (buffer-string) "hi")))))
(ert-deftest eshell-test/redirect-buffer-escaped ()
"Check that piping to a buffer with escaped characters works"
(with-temp-buffer
(rename-buffer "eshell\\temp\\buffer" t)
(let ((bufname (buffer-name)))
(with-temp-eshell
(eshell-insert-command (format "echo hi > #<%s>"
(string-replace "\\" "\\\\" bufname))))
(should (equal (buffer-string) "hi")))))
(ert-deftest eshell-test/escape-nonspecial ()
"Test that \"\\c\" and \"c\" are equivalent when \"c\" is not a
special character."