@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
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
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
@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.
-
-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.
-
-@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.
+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.
+
+@table @code
+
+@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
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
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)
(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."
(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
e.g. \"{(+ 1 2)} 3\" => 3"
(eshell-command-result-equal "{(+ 1 2)} 3" 3))
+\f
+;; 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))))
+
\f
;; Logical operators
--- /dev/null
+;;; 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:
+
+\f
+;; 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")))))
+
+\f
+;; 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"))))
+
+\f
+;; 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
(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
(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."