From d32c06617fee8e8572ac81ee12dc1da793cf3a87 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Sat, 9 Jul 2022 10:34:31 -0700 Subject: [PATCH 1/5] 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. --- doc/misc/eshell.texi | 160 ++++++++++++++--- lisp/eshell/esh-arg.el | 4 + lisp/eshell/esh-io.el | 55 +++--- test/lisp/eshell/esh-cmd-tests.el | 19 ++ test/lisp/eshell/esh-io-tests.el | 220 +++++++++++++++++++++++ test/lisp/eshell/eshell-tests-helpers.el | 10 ++ test/lisp/eshell/eshell-tests.el | 19 -- 7 files changed, 413 insertions(+), 74 deletions(-) create mode 100644 test/lisp/eshell/esh-io-tests.el diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index 13f13163dd..0c98d2860e 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -256,7 +256,6 @@ 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 @@ Arguments 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 # +@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 # +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 @@ Input/Output 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 @@ Input/Output @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 >>> #} -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 diff --git a/lisp/eshell/esh-arg.el b/lisp/eshell/esh-arg.el index 8e44a88459..50fb7f5fdc 100644 --- a/lisp/eshell/esh-arg.el +++ b/lisp/eshell/esh-arg.el @@ -147,6 +147,10 @@ eshell-parse-argument-hook 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 diff --git a/lisp/eshell/esh-io.el b/lisp/eshell/esh-io.el index d54be55c13..f5dac2c81c 100644 --- a/lisp/eshell/esh-io.el +++ b/lisp/eshell/esh-io.el @@ -236,22 +236,21 @@ eshell-create-handles 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 @@ eshell-close-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." @@ -390,22 +407,6 @@ eshell-get-target (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 diff --git a/test/lisp/eshell/esh-cmd-tests.el b/test/lisp/eshell/esh-cmd-tests.el index 3a582965d6..92d785d7fd 100644 --- a/test/lisp/eshell/esh-cmd-tests.el +++ b/test/lisp/eshell/esh-cmd-tests.el @@ -73,6 +73,25 @@ esh-cmd-test/subcommand-lisp 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 diff --git a/test/lisp/eshell/esh-io-tests.el b/test/lisp/eshell/esh-io-tests.el new file mode 100644 index 0000000000..6cd2dff1c1 --- /dev/null +++ b/test/lisp/eshell/esh-io-tests.el @@ -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 . + +;;; 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 diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el index 8f0f993447..73abfcbb55 100644 --- a/test/lisp/eshell/eshell-tests-helpers.el +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -51,6 +51,16 @@ with-temp-eshell (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 diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 1845dba280..d5112146c2 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -105,25 +105,6 @@ eshell-test/lisp-reset-in-pipeline (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." -- 2.25.1