* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining @ 2021-02-06 20:06 Sean Whitton 2021-02-07 9:17 ` Michael Albinus 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-02-06 20:06 UTC (permalink / raw) To: 46351 [-- Attachment #1: Type: text/plain, Size: 1359 bytes --] Most people who use Eshell a lot need to be aware of the limitations of its support for shell pipelines: primarily, that all data in pipelines has to go through Emacs buffers, and that can be slow. If you're running pipelines which will move a lot of data, you should use a system shell which will be more performant. Currently, if Eshell is your primary shell, it's not particularly convenient to switch to using an operating system shell when you're preparing a pipeline that you know is going to move a lot of data. You would need either to switch to a shell-mode buffer, or quote and escape your Eshell input and put something like `bash -c' in front of it. I think that it would be good to have a way to easily toggle Eshell's own pipelining support on and off for particular commands. I have come up with the attached patches. When the new functions I've defined are enabled, you can type !! foo | bar 'arg' >baz >>#<buffer *scratch*> and it will be executed by Eshell as if you had typed bash -c 'foo | bar '"'"'arg'"'"' >baz' >>#<buffer *scratch*> The idea is that you can easily toggle Eshell's pipelining on and off as appropriate to your needs just by adding and removing the "!!" prefix. I think that the patches I've prepared are a clean implementation of this feature that would be good to include in Emacs. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: 0001-Add-eshell-restore-unexpanded-input.patch --] [-- Type: text/x-diff, Size: 2458 bytes --] From ccc4427ff06bf6dc63840517aefbb7fb899d4b8c Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 6 Feb 2021 00:01:50 -0700 Subject: [PATCH 1/2] Add eshell-restore-unexpanded-input * lisp/eshell/esh-mode.el (eshell-send-input): Store the original input before running eshell-expand-input-functions. * lisp/eshell/esh-mode.el (eshell-restore-unexpanded-input): Define new function and register as a customization option for eshell-input-filter-functions. --- lisp/eshell/esh-mode.el | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el index d29b010ea0..6036829941 100644 --- a/lisp/eshell/esh-mode.el +++ b/lisp/eshell/esh-mode.el @@ -197,6 +197,7 @@ This is used by `eshell-watch-for-password-prompt'." ;; byte-compiler, when compiling other files which `require' this one (defvar eshell-mode nil) (defvar eshell-command-running-string "--") +(defvar eshell-last-unexpanded-input nil) (defvar eshell-last-input-start nil) (defvar eshell-last-input-end nil) (defvar eshell-last-output-start nil) @@ -631,6 +632,7 @@ newline." (progn (setq input (buffer-substring-no-properties eshell-last-output-end (1- (point)))) + (setq eshell-last-unexpanded-input input) (run-hook-with-args 'eshell-expand-input-functions eshell-last-output-end (1- (point))) (let ((cmd (eshell-parse-command-input @@ -664,6 +666,21 @@ newline." (custom-add-option 'eshell-input-filter-functions 'eshell-kill-new) +(defun eshell-restore-unexpanded-input () + "Restore the input text before `eshell-expand-input-functions' ran. +Useful when you want to see the unexpanded input rather than the +expanded input in the Eshell buffer, and when you want later +entries in `eshell-input-filter-functions' to use the unexpanded +input. For example, you might want the unexpanded input to be +what gets stored in the history list." + (setf (buffer-substring eshell-last-input-start + (1- eshell-last-input-end)) + eshell-last-unexpanded-input) + (eshell-update-markers eshell-last-output-end)) + +(custom-add-option 'eshell-input-filter-functions + 'eshell-restore-unexpanded-input) + (defun eshell-output-filter (process string) "Send the output from PROCESS (STRING) to the interactive display. This is done after all necessary filtering has been done." -- 2.29.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #3: 0002-Add-eshell-shell-command-and-eshell-expand-to-eshell.patch --] [-- Type: text/x-diff, Size: 4354 bytes --] From d5d48427c75555cd5cf2f6eb5b89e0c832cf7edf Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 6 Feb 2021 00:48:32 -0700 Subject: [PATCH 2/2] Add eshell-shell-command and eshell-expand-to-eshell-shell-command * lisp/eshell/esh-mode.el (eshell-shell-command, eshell-expand-to-eshell-shell-command): Define new functions. Register eshell-expand-to-eshell-shell-command as a customization option for eshell-expand-input-functions. --- lisp/eshell/esh-mode.el | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el index 6036829941..b3132393a5 100644 --- a/lisp/eshell/esh-mode.el +++ b/lisp/eshell/esh-mode.el @@ -62,6 +62,7 @@ (require 'esh-module) (require 'esh-cmd) (require 'esh-arg) ;For eshell-parse-arguments +(require 'subr-x) (defgroup eshell-mode nil "This module contains code for handling input from the user." @@ -660,6 +661,82 @@ newline." (run-hooks 'eshell-post-command-hook) (insert-and-inherit input))))))))) +(defun eshell-shell-command (command) + "Execute COMMAND using the operating system shell." + (throw 'eshell-replace-command + (eshell-parse-command shell-file-name + (list shell-command-switch command)))) + +(defun eshell-expand-to-eshell-shell-command (beg end) + "Expand Eshell input starting with '!!' to use `eshell-shell-command'. + +This is intended to provide a convenient way to bypass Eshell's +own pipelining which can be slow when executing shell pipelines +which move a lot of data. It is also useful to avoid +roundtripping data when running pipelines on remote hosts. + +To enable, add this function to `eshell-expand-input-functions'. + +For example, this function expands the input + + !! cat *.ogg | my-cool-decoder >file + +to + + eshell-shell-command \"cat *.ogg | my-cool-decoder >file\" + +Executing the latter command will not copy all the data in the +*.ogg files into Emacs buffers, as would normally happen with +Eshell's own pipelining. + +This function tries to extract Eshell-specific redirects and +avoids passing these to the operating system shell. For example, + + !! foo | bar >>#<buffer scratch> + +will be expanded to + + eshell-shell-command \"foo | bar\" >>#<buffer scratch> + +This function works well in combination with adding +`eshell-restore-unexpanded-input' to `eshell-input-filter-functions'." + (save-excursion + (goto-char beg) + (when (looking-at "!!\\s-*") + (let ((end (copy-marker end)) ; needs to be a marker + redirects) + ;; drop the !! + (delete-region beg (match-end 0)) + ;; extract Eshell-specific redirects + (while (search-forward-regexp "[0-9]?>+&?[0-9]?\\s-*\\S-" nil t) + (let ((beg (match-beginning 0))) + (forward-char -1) ; start from the redirect target + (when-let ((end (cond + ;; this is a redirect to a process or a + ;; buffer + ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ;; this is a redirect to a virtual target + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0))))) + (push (buffer-substring-no-properties beg end) redirects) + (delete-region beg end) + (just-one-space)))) + ;; wrap the remaining text and reinstate Eshell redirects + (let ((pipeline (string-trim + (buffer-substring-no-properties beg end)))) + (delete-region beg end) + (insert "eshell-shell-command ") + (prin1 pipeline (current-buffer)) + (when redirects + (insert " " (string-join (nreverse redirects) " ")))))))) + +(custom-add-option 'eshell-expand-input-functions + 'eshell-expand-to-eshell-shell-command) + (defsubst eshell-kill-new () "Add the last input text to the kill ring." (kill-ring-save eshell-last-input-start eshell-last-input-end)) -- 2.29.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-06 20:06 bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Sean Whitton @ 2021-02-07 9:17 ` Michael Albinus 2021-02-07 19:01 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2021-02-07 9:17 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: Hi Sean, > I think that it would be good to have a way to easily toggle Eshell's > own pipelining support on and off for particular commands. I have come > up with the attached patches. When the new functions I've defined are > enabled, you can type > > !! foo | bar 'arg' >baz >>#<buffer *scratch*> > > and it will be executed by Eshell as if you had typed > > bash -c 'foo | bar '"'"'arg'"'"' >baz' >>#<buffer *scratch*> > > The idea is that you can easily toggle Eshell's pipelining on and off as > appropriate to your needs just by adding and removing the "!!" prefix. Nice idea. > I think that the patches I've prepared are a clean implementation of > this feature that would be good to include in Emacs. Applying your patch, I get the compiler warning --8<---------------cut here---------------start------------->8--- ELC eshell/esh-mode.elc In end of data: eshell/esh-mode.el:1114:1: Warning: the function ‘(setf buffer-substring)’ is not known to be defined. --8<---------------cut here---------------end--------------->8--- And using it in eshell, there is --8<---------------cut here---------------start------------->8--- ~/src/emacs $ !! cat ~/.emacs | grep albinus !!: command not found --8<---------------cut here---------------end--------------->8--- I wanted to see, whether this works also for remote directories. Have you tested this? > Sean Whitton Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-07 9:17 ` Michael Albinus @ 2021-02-07 19:01 ` Sean Whitton 2021-02-08 10:28 ` Michael Albinus 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-02-07 19:01 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 On Sun 07 Feb 2021 at 10:17AM +01, Michael Albinus wrote: > Applying your patch, I get the compiler warning > > --8<---------------cut here---------------start------------->8--- > ELC eshell/esh-mode.elc > > In end of data: > eshell/esh-mode.el:1114:1: Warning: the function ‘(setf buffer-substring)’ is > not known to be defined. > --8<---------------cut here---------------end--------------->8--- Hrm, I can't reproduce this, but looking at the docs, I think that the problem is a missing (require 'cl-lib). Would you mind seeing whether that eliminates the warning at your end? > And using it in eshell, there is > > --8<---------------cut here---------------start------------->8--- > ~/src/emacs $ !! cat ~/.emacs | grep albinus > !!: command not found > --8<---------------cut here---------------end--------------->8--- You need to add eshell-expand-to-eshell-shell-command to eshell-expand-input-functions and (optionally) eshell-restore-unexpanded-input to eshell-input-filter-functions. Or maybe (the first of) these should be added by default? What do you think? For testing you can type these forms into an Eshell buffer: (add-hook 'eshell-expand-input-functions #'eshell-expand-to-eshell-shell-command nil t) (add-hook 'eshell-input-filter-functions #'eshell-restore-unexpanded-input nil t) > I wanted to see, whether this works also for remote directories. Have > you tested this? Yes, I have, and it works (I did cat largefile >file and confirmed it's much faster when prefixed with !!, of course because the data doesn't get copied to the local machine). -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-07 19:01 ` Sean Whitton @ 2021-02-08 10:28 ` Michael Albinus 2021-02-08 18:07 ` Sean Whitton 2021-12-24 21:20 ` Sean Whitton 0 siblings, 2 replies; 60+ messages in thread From: Michael Albinus @ 2021-02-08 10:28 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: Hi Sean, >> Applying your patch, I get the compiler warning >> >> --8<---------------cut here---------------start------------->8--- >> ELC eshell/esh-mode.elc >> >> In end of data: >> eshell/esh-mode.el:1114:1: Warning: the function ‘(setf buffer-substring)’ is >> not known to be defined. >> --8<---------------cut here---------------end--------------->8--- > > Hrm, I can't reproduce this, but looking at the docs, I think that the > problem is a missing (require 'cl-lib). Would you mind seeing whether > that eliminates the warning at your end? Yes, that helps. >> And using it in eshell, there is >> >> --8<---------------cut here---------------start------------->8--- >> ~/src/emacs $ !! cat ~/.emacs | grep albinus >> !!: command not found >> --8<---------------cut here---------------end--------------->8--- > > You need to add eshell-expand-to-eshell-shell-command to > eshell-expand-input-functions and (optionally) > eshell-restore-unexpanded-input to eshell-input-filter-functions. Or > maybe (the first of) these should be added by default? What do you think? > > For testing you can type these forms into an Eshell buffer: > > (add-hook 'eshell-expand-input-functions #'eshell-expand-to-eshell-shell-command nil t) > (add-hook 'eshell-input-filter-functions #'eshell-restore-unexpanded-input nil t) That works. Maybe we could add it to the defaults. But at least I would like to see it documented in the eshell manual, otherwise nobody will know the effect of "!!". And I have the impression, that the eshell history is not preserved any longer, when I have applied these settings. >> I wanted to see, whether this works also for remote directories. Have >> you tested this? > > Yes, I have, and it works (I did cat largefile >file and confirmed it's > much faster when prefixed with !!, of course because the data doesn't > get copied to the local machine). It doesn't work for me. The reason is a little bit subtle: you use `shell-file-name' in order to call a remote shell. In my case, it is "/usr/local/bin/tcsh", which doesn't exist on the remote machine I've used for testing. Maybe you can base your implementation on `shell-command'? This should know, how to handle a connection-local `shell-file-name'. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-08 10:28 ` Michael Albinus @ 2021-02-08 18:07 ` Sean Whitton 2021-02-10 11:33 ` Michael Albinus 2021-12-24 21:20 ` Sean Whitton 1 sibling, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-02-08 18:07 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 Hello, On Mon 08 Feb 2021 at 11:28AM +01, Michael Albinus wrote: > Sean Whitton <spwhitton@spwhitton.name> writes: >> >> Hrm, I can't reproduce this, but looking at the docs, I think that the >> problem is a missing (require 'cl-lib). Would you mind seeing whether >> that eliminates the warning at your end? > > Yes, that helps. Thanks, I will add it. > That works. Maybe we could add it to the defaults. But at least I would > like to see it documented in the eshell manual, otherwise nobody will > know the effect of "!!". I was waiting to document it in the manual based on what others thought the defaults should be. If neither hook is added by default then I think the docstrings of the functions that I've already written are adequate documentation. What do you think about adding only the eshell-expand-input-functions hook by default, versus adding both of them? I think that adding both is probably most useful to most people, but it has the potential to break existing uses of eshell-expand-input-functions in people's configs. I'll look into your other points, thank you. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-08 18:07 ` Sean Whitton @ 2021-02-10 11:33 ` Michael Albinus 0 siblings, 0 replies; 60+ messages in thread From: Michael Albinus @ 2021-02-10 11:33 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello, Hi Sean, >> That works. Maybe we could add it to the defaults. But at least I would >> like to see it documented in the eshell manual, otherwise nobody will >> know the effect of "!!". > > I was waiting to document it in the manual based on what others thought > the defaults should be. If neither hook is added by default then I > think the docstrings of the functions that I've already written are > adequate documentation. Sure. But the manual needs an explanation of "!!". > What do you think about adding only the eshell-expand-input-functions > hook by default, versus adding both of them? I think that adding both > is probably most useful to most people, but it has the potential to > break existing uses of eshell-expand-input-functions in people's > configs. Honestly, I'm not a heavy eshell user, so I don't know whether it will disturb people. If you don't feel comfortable with these changes by default, you might move them to an own eshell module, people would load if they like. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-02-08 10:28 ` Michael Albinus 2021-02-08 18:07 ` Sean Whitton @ 2021-12-24 21:20 ` Sean Whitton 2021-12-25 13:51 ` Michael Albinus 1 sibling, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-12-24 21:20 UTC (permalink / raw) To: 46351, Michael Albinus [-- Attachment #1: Type: text/plain, Size: 1134 bytes --] Hello, On Mon 08 Feb 2021 at 11:28AM +01, Michael Albinus wrote: > Maybe we could add it to the defaults. But at least I would > like to see it documented in the eshell manual, otherwise nobody will > know the effect of "!!". I'm attaching new patches which - switch !! to ||, because !! in shells typically has something to do with rerunning the last command - activate the syntax by default -- I do use Eshell regularly myself, and I don't believe that activating it by default is likely to disrupt anyone's usage - adds a section to the Eshell manual introducing the new syntax. > And I have the impression, that the eshell history is not preserved any > longer, when I have applied these settings. I have not been able to reproduce this -- can you confirm? > It doesn't work for me. The reason is a little bit subtle: you use > `shell-file-name' in order to call a remote shell. In my case, it is > "/usr/local/bin/tcsh", which doesn't exist on the remote machine I've > used for testing. I was able to fix this by using `with-connection-local-variables'. Thank you for pointing out the problem. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v2-0001-Add-eshell-restore-unexpanded-input.patch --] [-- Type: text/x-patch, Size: 2720 bytes --] From d47446cbe7db5c7fb4eba71ab022cfae3de07799 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 6 Feb 2021 00:01:50 -0700 Subject: [PATCH v2 1/2] Add eshell-restore-unexpanded-input * lisp/eshell/esh-mode.el (eshell-send-input): Store the original input before running eshell-expand-input-functions. * lisp/eshell/esh-mode.el (eshell-restore-unexpanded-input): Define new function and register as a customization option for eshell-input-filter-functions. --- lisp/eshell/esh-mode.el | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el index cae5236d89..87166ef3bf 100644 --- a/lisp/eshell/esh-mode.el +++ b/lisp/eshell/esh-mode.el @@ -62,6 +62,7 @@ (require 'esh-module) (require 'esh-cmd) (require 'esh-arg) ;For eshell-parse-arguments +(require 'cl-lib) (defgroup eshell-mode nil "This module contains code for handling input from the user." @@ -197,6 +198,7 @@ eshell-first-time-p ;; byte-compiler, when compiling other files which `require' this one (defvar eshell-mode nil) (defvar eshell-command-running-string "--") +(defvar eshell-last-unexpanded-input nil) (defvar eshell-last-input-start nil) (defvar eshell-last-input-end nil) (defvar eshell-last-output-start nil) @@ -641,6 +643,7 @@ eshell-send-input (progn (setq input (buffer-substring-no-properties eshell-last-output-end (1- (point)))) + (setq eshell-last-unexpanded-input input) (run-hook-with-args 'eshell-expand-input-functions eshell-last-output-end (1- (point))) (let ((cmd (eshell-parse-command-input @@ -674,6 +677,23 @@ eshell-kill-new (custom-add-option 'eshell-input-filter-functions 'eshell-kill-new) +(defun eshell-restore-unexpanded-input () + "Restore the input text before `eshell-expand-input-functions' ran. +Useful when you want to see the unexpanded input rather than the +expanded input in the Eshell buffer, and when you want later +entries in `eshell-input-filter-functions' to use the unexpanded +input." + (setf (buffer-substring eshell-last-input-start + (1- eshell-last-input-end)) + eshell-last-unexpanded-input) + ;; We have to move point for compatibility with smart display. + (save-excursion + (goto-char eshell-last-input-end) + (eshell-update-markers eshell-last-output-end))) + +(custom-add-option 'eshell-input-filter-functions + 'eshell-restore-unexpanded-input) + (defun eshell-output-filter (process string) "Send the output from PROCESS (STRING) to the interactive display. This is done after all necessary filtering has been done." -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #3: v2-0002-Add-eshell-shell-command-and-eshell-expand-to-esh.patch --] [-- Type: text/x-patch, Size: 7533 bytes --] From 98df5163018b9c99ad8e65f03422f908c6068c78 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 6 Feb 2021 00:48:32 -0700 Subject: [PATCH v2 2/2] Add eshell-shell-command and eshell-expand-to-eshell-shell-command * lisp/eshell/esh-mode.el (eshell-shell-command, eshell-expand-to-eshell-shell-command): Define new functions. Register eshell-expand-to-eshell-shell-command as a customization option for eshell-expand-input-functions, and add it by default. * etc/NEWS: * doc/misc/eshell.texi: Document the new syntax. --- doc/misc/eshell.texi | 35 ++++++++++++++++++ etc/NEWS | 9 +++++ lisp/eshell/esh-mode.el | 79 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index a87dd4308c..7e9233c09e 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -875,6 +875,7 @@ Expansion @menu * Dollars Expansion:: * Globbing:: +* Running Shell Pipelines Natively:: @end menu @node Dollars Expansion @@ -945,6 +946,40 @@ Globbing The GNU Emacs Manual}.} groups ``eshell-glob'' and ``eshell-pred''. +@node Running Shell Pipelines Natively +@section 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 +executing commands on a remote machine using Eshell's Tramp +integration: using the remote shell's pipelining avoids copying the +data which will flow through the pipeline to local Emacs buffers and +then right back again. + +To quickly construct a command for the native shell, prefix your +Eshell input with the string @code{||}. For example, + +@example +|| cat *.ogg | my-cool-decoder >file +@end example + +Executing this command will not copy all the data in the *.ogg files +into Emacs buffers, as would normally happen with Eshell's own +pipelining. + +As this feature is implemented as an expansion, your original input +will be replaced with something like this: + +@example +eshell-shell-command \"cat *.ogg | my-cool-decoder >file\" +@end example + +This makes it clear what Eshell is doing, but has the disadvantage of +being harder to edit, especially for complex pipelines with a lot of +quoting. If you would prefer that the @code{||}-prefixed input be +restored, customize the variable @code{eshell-input-filter-functions} +to include the function @code{eshell-restore-unexpanded-input}. + @node Input/Output @chapter Input/Output Since Eshell does not communicate with a terminal like most command diff --git a/etc/NEWS b/etc/NEWS index 57fe40c488..767763098b 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -749,6 +749,15 @@ the Netscape web browser was released in February, 2008. This support has been obsolete since Emacs 25.1. The final version of the Galeon web browser was released in September, 2008. +** Eshell + ++++ +*** New feature to easily bypass Eshell's own pipelining. +Prefixing commands with '||' causes them to be executed using the +operating system shell, which is particularly useful to bypass +Eshell's own pipelining for pipelines which will move a lot of data. +See "Running Shell Pipelines Natively" in the Eshell manual. + \f * New Modes and Packages in Emacs 29.1 diff --git a/lisp/eshell/esh-mode.el b/lisp/eshell/esh-mode.el index 87166ef3bf..04680e4305 100644 --- a/lisp/eshell/esh-mode.el +++ b/lisp/eshell/esh-mode.el @@ -63,6 +63,7 @@ (require 'esh-cmd) (require 'esh-arg) ;For eshell-parse-arguments (require 'cl-lib) +(require 'subr-x) (defgroup eshell-mode nil "This module contains code for handling input from the user." @@ -105,7 +106,8 @@ eshell-send-direct-to-subprocesses "If t, send any input immediately to a subprocess." :type 'boolean) -(defcustom eshell-expand-input-functions nil +(defcustom eshell-expand-input-functions + '(eshell-expand-to-eshell-shell-command) "Functions to call before input is parsed. Each function is passed two arguments, which bounds the region of the current input text." @@ -671,6 +673,81 @@ eshell-send-input (run-hooks 'eshell-post-command-hook) (insert-and-inherit input))))))))) +(defun eshell-shell-command (command) + "Execute COMMAND using the operating system shell." + (throw 'eshell-replace-command + (with-connection-local-variables + (eshell-parse-command shell-file-name + (list shell-command-switch command))))) + +(defun eshell-expand-to-eshell-shell-command (beg end) + "Expand Eshell input starting with '||' to use `eshell-shell-command'. + +This is intended to provide a convenient way to bypass Eshell's +own pipelining which can be slow when executing shell pipelines +which move a lot of data. It is also useful to avoid +roundtripping data when running pipelines on remote hosts. + +For example, this function expands the input + + || cat *.ogg | my-cool-decoder >file + +to + + eshell-shell-command \"cat *.ogg | my-cool-decoder >file\" + +Executing the latter command will not copy all the data in the +*.ogg files into Emacs buffers, as would normally happen with +Eshell's own pipelining. + +This function tries to extract Eshell-specific redirects and +avoids passing these to the operating system shell. For example, + + || foo | bar >>#<buffer scratch> + +will be expanded to + + eshell-shell-command \"foo | bar\" >>#<buffer scratch> + +This function works well in combination with adding +`eshell-restore-unexpanded-input' to `eshell-input-filter-functions'." + (save-excursion + (goto-char beg) + (when (looking-at "||\\s-*") + (let ((end (copy-marker end)) ; needs to be a marker + redirects) + ;; drop the || + (delete-region beg (match-end 0)) + ;; extract Eshell-specific redirects + (while (search-forward-regexp "[0-9]?>+&?[0-9]?\\s-*\\S-" nil t) + (let ((beg (match-beginning 0))) + (forward-char -1) ; start from the redirect target + (when-let ((end (cond + ;; this is a redirect to a process or a + ;; buffer + ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ;; this is a redirect to a virtual target + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0))))) + (push (buffer-substring-no-properties beg end) redirects) + (delete-region beg end) + (just-one-space)))) + ;; wrap the remaining text and reinstate Eshell redirects + (let ((pipeline (string-trim + (buffer-substring-no-properties beg end)))) + (delete-region beg end) + (insert "eshell-shell-command ") + (prin1 pipeline (current-buffer)) + (when redirects + (insert " " (string-join (nreverse redirects) " ")))))))) + +(custom-add-option 'eshell-expand-input-functions + 'eshell-expand-to-eshell-shell-command) + (defsubst eshell-kill-new () "Add the last input text to the kill ring." (kill-ring-save eshell-last-input-start eshell-last-input-end)) -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-24 21:20 ` Sean Whitton @ 2021-12-25 13:51 ` Michael Albinus 2021-12-25 22:45 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2021-12-25 13:51 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello, Hi Sean, >> Maybe we could add it to the defaults. But at least I would >> like to see it documented in the eshell manual, otherwise nobody will >> know the effect of "!!". > > I'm attaching new patches which > > - switch !! to ||, because !! in shells typically has something to do > with rerunning the last command > > - activate the syntax by default -- I do use Eshell regularly myself, > and I don't believe that activating it by default is likely to disrupt > anyone's usage > > - adds a section to the Eshell manual introducing the new syntax. Thanks! I've played a little bit with it. >> And I have the impression, that the eshell history is not preserved any >> longer, when I have applied these settings. > > I have not been able to reproduce this -- can you confirm? Yep, the eshell history is there. However, it looks a little bit weird, when I have applied '|| cat .emacs | grep a'. The history shows me 'eshell-shell-command "cat .emacs | grep a"' instead. I understand what's meant, but is this convenient for an occasional eshell user? >> It doesn't work for me. The reason is a little bit subtle: you use >> `shell-file-name' in order to call a remote shell. In my case, it is >> "/usr/local/bin/tcsh", which doesn't exist on the remote machine I've >> used for testing. > > I was able to fix this by using `with-connection-local-variables'. > Thank you for pointing out the problem. This is fixed, indeed. Thinking more about, perhaps we could solve it a little bit differently. Eshell knows how to invoke external commands explicitly, by prefixing the command with a star like '*ls'. What if we do it the same with pipes? That means, '*|' would mean to call an external pipe. So my command above would be 'cat .emacs *| grep a'. And an external pipe shall convert all other commands in that line into external commands as well, like '*cat .emacs *| *grep a'. By this you wouldn't need your restore-unexpanded-input patch any more, yes? WDYT? Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-25 13:51 ` Michael Albinus @ 2021-12-25 22:45 ` Sean Whitton 2021-12-27 14:42 ` Michael Albinus 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-12-25 22:45 UTC (permalink / raw) To: Michael Albinus, 46351 Hello Michael, On Sat 25 Dec 2021 at 02:51PM +01, Michael Albinus wrote: > Yep, the eshell history is there. However, it looks a little bit weird, > when I have applied '|| cat .emacs | grep a'. The history shows me > 'eshell-shell-command "cat .emacs | grep a"' instead. I understand > what's meant, but is this convenient for an occasional eshell user? Yeah. Actually, when I prepared the first version of this series, the unexpanded version got stored in the history, so I think something has changed about Eshell history recording in the past year. > Thinking more about, perhaps we could solve it a little bit > differently. Eshell knows how to invoke external commands explicitly, by > prefixing the command with a star like '*ls'. What if we do it the same > with pipes? That means, '*|' would mean to call an external pipe. So my > command above would be 'cat .emacs *| grep a'. And an external pipe > shall convert all other commands in that line into external commands as > well, like '*cat .emacs *| *grep a'. > > By this you wouldn't need your restore-unexpanded-input patch any more, yes? This is an intriguing suggestion. It would need to be implemented in `eshell-parse-pipeline, rather than `eshell-expand-input-functions', because parsing is required to determine which *| are unquoted, etc. We also require *> and *< redirections, also in `eshell-parse-pipeline'. So, I guess when A *| B is parsed, it would be rewritten to a single call to `shell-file-name' in the way that `eshell-shell-command' does. I am not sure how feasible it will be to implement that within the existing structure of `eshell-parse-pipeline', but I can give it a try. I have one unresolved design question. A disadvantage of your proposal compared to mine is that it is much more laborious to convert an existing pipeline to use the external shell: you have to replace each | with *| and > with *>, etc., instead of just putting || at the beginning. I think it's important that it is easy to switch this feature on and off, because it is often only after you start figuring out a pipeline that you realise you ought to bypass Eshell's pipelining. One option would be to add an entry to `eshell-expand-input-functions' which converts all | to *| and > to *> when the input begins with ||. But that is too simple, because you need to parse the input to know which | to replace ... any ideas? I don't think it can be done inside `eshell-parse-command'. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-25 22:45 ` Sean Whitton @ 2021-12-27 14:42 ` Michael Albinus 2021-12-27 18:13 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2021-12-27 14:42 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello Michael, Hi Sean, >> Thinking more about, perhaps we could solve it a little bit >> differently. Eshell knows how to invoke external commands explicitly, by >> prefixing the command with a star like '*ls'. What if we do it the same >> with pipes? That means, '*|' would mean to call an external pipe. So my >> command above would be 'cat .emacs *| grep a'. And an external pipe >> shall convert all other commands in that line into external commands as >> well, like '*cat .emacs *| *grep a'. >> >> By this you wouldn't need your restore-unexpanded-input patch any more, yes? > > This is an intriguing suggestion. It would need to be implemented in > `eshell-parse-pipeline, rather than `eshell-expand-input-functions', > because parsing is required to determine which *| are unquoted, etc. We > also require *> and *< redirections, also in `eshell-parse-pipeline'. > > So, I guess when A *| B is parsed, it would be rewritten to a single > call to `shell-file-name' in the way that `eshell-shell-command' does. > I am not sure how feasible it will be to implement that within the > existing structure of `eshell-parse-pipeline', but I can give it a try. Perhaps. I'm not so familar with the eshell code, so I cannot give you much recommendation on implementation details. > I have one unresolved design question. A disadvantage of your proposal > compared to mine is that it is much more laborious to convert an > existing pipeline to use the external shell: you have to replace each | > with *| and > with *>, etc., instead of just putting || at the > beginning. Yep, but this isn't relevant for the user. This replacement shall happen by eshell itself, in the background. And it isn't necessary to show this replacement by the commands in the history. > I think it's important that it is easy to switch this feature on and > off, because it is often only after you start figuring out a pipeline > that you realise you ought to bypass Eshell's pipelining. As I said: switching on this feature just needs the asterisk to *|. > One option would be to add an entry to `eshell-expand-input-functions' > which converts all | to *| and > to *> when the input begins with ||. > But that is too simple, because you need to parse the input to know > which | to replace ... any ideas? I don't think it can be done inside > `eshell-parse-command'. Again, I don't see the need for the heading ||. When there is a *| in the command line, DTRT. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 14:42 ` Michael Albinus @ 2021-12-27 18:13 ` Sean Whitton 2021-12-27 18:22 ` Eli Zaretskii 2021-12-27 18:26 ` Michael Albinus 0 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2021-12-27 18:13 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 Hello Michael, On Mon 27 Dec 2021 at 03:42PM +01, Michael Albinus wrote: >> I have one unresolved design question. A disadvantage of your proposal >> compared to mine is that it is much more laborious to convert an >> existing pipeline to use the external shell: you have to replace each | >> with *| and > with *>, etc., instead of just putting || at the >> beginning. > > Yep, but this isn't relevant for the user. This replacement shall happen > by eshell itself, in the background. And it isn't necessary to show this > replacement by the commands in the history. > >> I think it's important that it is easy to switch this feature on and >> off, because it is often only after you start figuring out a pipeline >> that you realise you ought to bypass Eshell's pipelining. > > As I said: switching on this feature just needs the asterisk to *|. > >> One option would be to add an entry to `eshell-expand-input-functions' >> which converts all | to *| and > to *> when the input begins with ||. >> But that is too simple, because you need to parse the input to know >> which | to replace ... any ideas? I don't think it can be done inside >> `eshell-parse-command'. > > Again, I don't see the need for the heading ||. When there is a *| in > the command line, DTRT. What I had in mind was a pipeline involving more than two commands, e.g. foo | bar | baz >quux Indeed, this is exactly the sort of pipeline you might want this feature for. But the user would have to manually insert three asterisks to turn on this feature. That seems pretty inconvenient -- any thoughts on how to improve it? -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 18:13 ` Sean Whitton @ 2021-12-27 18:22 ` Eli Zaretskii 2021-12-27 19:21 ` Sean Whitton 2021-12-27 18:26 ` Michael Albinus 1 sibling, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2021-12-27 18:22 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Date: Mon, 27 Dec 2021 11:13:29 -0700 > Cc: 46351@debbugs.gnu.org > > What I had in mind was a pipeline involving more than two commands, e.g. > > foo | bar | baz >quux > > Indeed, this is exactly the sort of pipeline you might want this feature > for. But the user would have to manually insert three asterisks to turn > on this feature. That seems pretty inconvenient -- any thoughts on how > to improve it? The easiest way is to add a user option that would make every pipe use the external shell. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 18:22 ` Eli Zaretskii @ 2021-12-27 19:21 ` Sean Whitton 2021-12-27 19:35 ` Eli Zaretskii 2021-12-27 19:37 ` Michael Albinus 0 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2021-12-27 19:21 UTC (permalink / raw) To: Michael Albinus, Eli Zaretskii, 46351 Hello, On Mon 27 Dec 2021 at 08:22PM +02, Eli Zaretskii wrote: > The easiest way is to add a user option that would make every pipe use > the external shell. On Mon 27 Dec 2021 at 07:26PM +01, Michael Albinus wrote: > One to rule them all. As soon there is one *|, *< or *> in the command > line, everything is regarded external. Also commands like ls or cat, > which behave then like *ls or *cat. I see what you mean now, but I don't think either of these options would be desirable. It would mean that you can't combine Lisp functions with external commands; for example my-lisp-function arg1 arg2 | my-cool-encoder *>output.ogg In this case, the first | needs to use Eshell's own pipelining support. Similarly something like buffer-string #<buffer ...> | my-cool-command *| other-cmd *>file Does that make sense? Would you agree that this new feature needs to work only on individual pairs of commands? -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 19:21 ` Sean Whitton @ 2021-12-27 19:35 ` Eli Zaretskii 2021-12-27 19:53 ` Sean Whitton 2021-12-27 19:37 ` Michael Albinus 1 sibling, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2021-12-27 19:35 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Date: Mon, 27 Dec 2021 12:21:53 -0700 > > On Mon 27 Dec 2021 at 08:22PM +02, Eli Zaretskii wrote: > > > The easiest way is to add a user option that would make every pipe use > > the external shell. > > On Mon 27 Dec 2021 at 07:26PM +01, Michael Albinus wrote: > > > One to rule them all. As soon there is one *|, *< or *> in the command > > line, everything is regarded external. Also commands like ls or cat, > > which behave then like *ls or *cat. > > I see what you mean now, but I don't think either of these options would > be desirable. It would mean that you can't combine Lisp functions with > external commands; for example > > my-lisp-function arg1 arg2 | my-cool-encoder *>output.ogg > > In this case, the first | needs to use Eshell's own pipelining support. > > Similarly something like > > buffer-string #<buffer ...> | my-cool-command *| other-cmd *>file > > Does that make sense? Would you agree that this new feature needs to > work only on individual pairs of commands? The *| thingy could be available as well, for when you need a mix. My proposal was only for the situation when it's hard or inconvenient to change too many pipes manually. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 19:35 ` Eli Zaretskii @ 2021-12-27 19:53 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2021-12-27 19:53 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Mon 27 Dec 2021 at 09:35PM +02, Eli Zaretskii wrote: > The *| thingy could be available as well, for when you need a mix. My > proposal was only for the situation when it's hard or inconvenient to > change too many pipes manually. Ah right, I see -- yes, nice idea. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 19:21 ` Sean Whitton 2021-12-27 19:35 ` Eli Zaretskii @ 2021-12-27 19:37 ` Michael Albinus 2021-12-27 19:54 ` Sean Whitton 1 sibling, 1 reply; 60+ messages in thread From: Michael Albinus @ 2021-12-27 19:37 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello, Hi Sean, >> One to rule them all. As soon there is one *|, *< or *> in the command >> line, everything is regarded external. Also commands like ls or cat, >> which behave then like *ls or *cat. > > I see what you mean now, but I don't think either of these options would > be desirable. It would mean that you can't combine Lisp functions with > external commands; for example > > my-lisp-function arg1 arg2 | my-cool-encoder *>output.ogg > > In this case, the first | needs to use Eshell's own pipelining support. > > Similarly something like > > buffer-string #<buffer ...> | my-cool-command *| other-cmd *>file > > Does that make sense? Would you agree that this new feature needs to > work only on individual pairs of commands? If you want this complexity (external and internal pipelins and stdin/stout redirection) in one command line, you must mark every single such operator with an asterisk, which might be inconvenient. But you have the eshell history, which gives you the command line including the external asterisks, which let you edit the command prior reexecution. Personally, I could live with both approaches. Your proposal with a leading "||" is similar to my "one rules them all", because it changes the meaning of all pipelines etc in a command line to be external. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 19:37 ` Michael Albinus @ 2021-12-27 19:54 ` Sean Whitton 2021-12-28 8:58 ` Michael Albinus 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2021-12-27 19:54 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 Hello Michael, On Mon 27 Dec 2021 at 08:37PM +01, Michael Albinus wrote: > If you want this complexity (external and internal pipelins and > stdin/stout redirection) in one command line, you must mark every single > such operator with an asterisk, which might be inconvenient. But you > have the eshell history, which gives you the command line including the > external asterisks, which let you edit the command prior reexecution. > > Personally, I could live with both approaches. Your proposal with a > leading "||" is similar to my "one rules them all", because it changes > the meaning of all pipelines etc in a command line to be external. Right. I was thinking that the piecemeal approach was one of the advantages of your idea. I'll see if I can implement this more complex thing and get back to you. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 19:54 ` Sean Whitton @ 2021-12-28 8:58 ` Michael Albinus 2022-01-18 5:19 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2021-12-28 8:58 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello Michael, Hi Sean, >> If you want this complexity (external and internal pipelins and >> stdin/stout redirection) in one command line, you must mark every single >> such operator with an asterisk, which might be inconvenient. But you >> have the eshell history, which gives you the command line including the >> external asterisks, which let you edit the command prior reexecution. >> >> Personally, I could live with both approaches. Your proposal with a >> leading "||" is similar to my "one rules them all", because it changes >> the meaning of all pipelines etc in a command line to be external. > > Right. I was thinking that the piecemeal approach was one of the > advantages of your idea. Yes, it would be. But it is more complex then. If you have an external pipe *|, a built-in command on the LHS of the pipe must be external as well. cat behaves already like this in pipelines, perhaps we shall request this for all built-in commands. > I'll see if I can implement this more complex thing and get back to you. Yes, please do. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-28 8:58 ` Michael Albinus @ 2022-01-18 5:19 ` Sean Whitton 2022-01-18 9:49 ` Robert Pluim ` (3 more replies) 0 siblings, 4 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-18 5:19 UTC (permalink / raw) To: Michael Albinus, 46351 [-- Attachment #1: Type: text/plain, Size: 307 bytes --] Hello, On Tue 28 Dec 2021 at 09:58AM +01, Michael Albinus wrote: > Sean Whitton <spwhitton@spwhitton.name> writes: > >> I'll see if I can implement this more complex thing and get back to you. > > Yes, please do. I've got it working. Please let me know what you think of the attached. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v3-0001-Add-Eshell-syntax-to-more-easily-bypass-Eshell-s-.patch --] [-- Type: text/x-patch, Size: 15907 bytes --] From 09c2cb2b43e9b988b682d13da8bf486a0a354333 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 17 Jan 2022 15:15:36 -0700 Subject: [PATCH v3] Add Eshell syntax to more easily bypass Eshell's own pipelining * etc/NEWS: * doc/misc/eshell.texi (Input/Output): Document the new syntax. * lisp/eshell/em-extpipe.el: New module. * test/lisp/eshell/em-extpipe-tests.el: New tests. * lisp/eshell/esh-module.el (eshell-modules-list): Add `eshell-extpipe'. --- doc/misc/eshell.texi | 39 ++++++ etc/NEWS | 10 ++ lisp/eshell/em-extpipe.el | 175 +++++++++++++++++++++++++++ lisp/eshell/esh-module.el | 1 + test/lisp/eshell/em-extpipe-tests.el | 122 +++++++++++++++++++ 5 files changed, 347 insertions(+) create mode 100644 lisp/eshell/em-extpipe.el create mode 100644 test/lisp/eshell/em-extpipe-tests.el diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index f1d7c63805..d19fe6fa80 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1135,6 +1135,45 @@ Input/Output 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 +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 +executing commands on a remote machine using Eshell's Tramp +integration: using the remote shell's pipelining avoids copying the +data which will flow through the pipeline to local Emacs buffers and +then right back again. + +Eshell recognises a special syntax to make it easier to convert +pipelines so as to bypass Eshell's pipelining. Prefixing at least one +@code{|}, @code{<} or @code{>} with an asterisk marks a command as +intended for the operating system shell. For example, + +@example + cat *.ogg *| my-cool-decoder >file +@end example + +Executing this command will not copy all the data in the *.ogg files, +nor the decoded data, into Emacs buffers, as would normally happen. + +The command is interpreted as extending up to the next +asterisk-unprefixed @code{|} character, or the end of the input if +there is no such character. Thus, all @code{<} and @code{>} +redirections occuring before the next asterisk-unprefixed @code{|} are +implicitly prefixed with asterisks. An exception is that +Eshell-specific redirects right at the end of the command are +excluded. This allows input like this: + +@example + foo *| baz >#<buffer quux> +@end example + +@noindent which is equivalent to input like this: + +@example + sh -c "foo | baz" >#<buffer quux> +@end example + @node Extension modules @chapter Extension modules Eshell provides a facility for defining extension modules so that they diff --git a/etc/NEWS b/etc/NEWS index fdbfd9b1be..144844dc6a 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -834,6 +834,16 @@ the Netscape web browser was released in February, 2008. This support has been obsolete since Emacs 25.1. The final version of the Galeon web browser was released in September, 2008. +** Eshell + ++++ +*** New feature to easily bypass Eshell's own pipelining. +Prefixing '|', '<' or '>' with an asterisk, i.e. '*|', '*<' or '*>', +will cause the whole command to be passed to the operating system +shell. This is particularly useful to bypass Eshell's own pipelining +support for pipelines which will move a lot of data. See "Running +Shell Pipelines Natively" in the Eshell manual. + ** Miscellaneous --- diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el new file mode 100644 index 0000000000..a877c5624e --- /dev/null +++ b/lisp/eshell/em-extpipe.el @@ -0,0 +1,175 @@ +;;; em-extpipe.el --- external shell pipelines -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + +;; 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 module tries to make +;; that easy to do. + +;;; Code: + +(require 'cl-lib) +(require 'esh-arg) +(require 'esh-io) +(require 'esh-util) + +(eval-when-compile (require 'files-x)) + +;;; Functions: + +(defun eshell-extpipe-initialize () ;Called from `eshell-mode' via intern-soft! + "Initialize external pipelines support." + (when (boundp 'eshell-special-chars-outside-quoting) + (setq-local + eshell-special-chars-outside-quoting + (append eshell-special-chars-outside-quoting (list ?\*)))) + (add-hook 'eshell-parse-argument-hook + #'eshell-parse-external-pipeline -20 t) + (add-hook 'eshell-pre-rewrite-command-hook + #'eshell-rewrite-external-pipeline -20 t)) + +(defun eshell-parse-external-pipeline () + "Parse a pipeline intended for execution by the external shell. + +A sequence of arguments is rewritten to use the operating system +shell when it contains `*|', `*<' or `*>'. The command extends +to the next `|' character which is not preceded by an unescaped +asterisk, or the end of input, except that any Eshell-specific +output redirections occurring at the end are excluded. Any other +`<' or `>' appearing before the end of the command are treated as +though preceded by an asterisk. + +For example, + + foo <bar *| baz >#<buffer quux> + +is equivalent to + + sh -c \"foo <bar | baz\" >#<buffer quux> + +when `shell-file-name' is `sh' and `shell-command-switch' is +`-c', but in + + foo >#<buffer quux> *| baz + +and + + foo *| baz >#<buffer quux> --some-argument + +the Eshell-specific redirect will be passed on to the operating +system shell, probably leading to undesired results. + +This function must appear early in `eshell-parse-argument-hook' +to ensure that operating system shell syntax is not interpreted +as though it were Eshell syntax." + ;; Our aim is to wrap the external command to protect it from the + ;; other members of `eshell-parse-argument-hook'. We must avoid + ;; misinterpreting a quoted `*|', `*<' or `*>' as indicating an + ;; external pipeline, hence the structure of the loop in `findbeg'. + (cl-flet + ((findbeg (pat &optional go (bound (point-max))) + (let* ((start (point)) + (result + (catch 'found + (while (> bound (point)) + (let* ((found + (save-excursion + (re-search-forward "['\"\\]" bound t))) + (next (or (and found (match-beginning 0)) + bound))) + (if (re-search-forward pat next t) + (throw 'found (match-beginning 0)) + (goto-char next) + (while (or (eshell-parse-backslash) + (eshell-parse-double-quote) + (eshell-parse-literal-quote))))))))) + (goto-char (if (and result go) (match-end 0) start)) + result))) + (unless (or eshell-current-argument eshell-current-quoted) + (let ((beg (point)) end + (next-marked (findbeg "\\*[|<>]")) + (next-unmarked + (or (findbeg "\\(\\=\\|[^*]\\)|") (point-max)))) + (when (and next-marked (> next-unmarked next-marked)) + ;; Skip to the final segment of the external pipeline. + (while (findbeg "\\*|" t)) + ;; Find output redirections. + (while (findbeg "[0-9]?>+&?[0-9]?\\s-*\\S-" t next-unmarked) + ;; Is the output redirection Eshell-specific? We have our + ;; own logic, rather than calling `eshell-parse-argument', + ;; to avoid specifying here all the possible cars of + ;; parsed special references -- `get-buffer-create' etc. + (forward-char -1) + (let ((this-end + (save-match-data + (cond ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0)))))) + (cond ((and this-end end) + (goto-char this-end)) + (this-end + (goto-char this-end) + (setq end (match-beginning 0))) + (t + (setq end nil))))) + ;; We've moved past all Eshell-specific output redirections + ;; we could find. If there is only whitespace left, then + ;; `end' is right before redirections we should exclude; + ;; otherwise, we must include everything. + (unless (and end (skip-syntax-forward "\s" next-unmarked) + (= next-unmarked (point))) + (setq end next-unmarked)) + (let ((cmd (string-trim + (buffer-substring-no-properties beg end)))) + (goto-char end) + ;; We must now drop the asterisks, unless quoted/escaped. + (with-temp-buffer + (insert cmd) + (goto-char (point-min)) + (cl-loop for next = (findbeg "\\*[|<>]" t) while next + do (forward-char -2) (delete-char 1)) + (eshell-finish-arg + `(eshell-external-pipeline ,(buffer-string)))))))))) + +(defun eshell-rewrite-external-pipeline (terms) + "Rewrite an external pipeline in TERMS as parsed by +`eshell-parse-external-pipeline', which see." + (while terms + (when (and (listp (car terms)) + (eq (caar terms) 'eshell-external-pipeline)) + (with-connection-local-variables + (setcdr terms (cl-list* + shell-command-switch (cadar terms) (cdr terms))) + (setcar terms shell-file-name))) + (setq terms (cdr terms)))) + +(defsubst eshell-external-pipeline (&rest _args) + "Stub to generate an error if a pipeline is not rewritten." + (error "Unhandled external pipeline in input text")) + +(provide 'em-extpipe) +;;; esh-extpipe.el ends here diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el index ade151d7cd..14e91912d1 100644 --- a/lisp/eshell/esh-module.el +++ b/lisp/eshell/esh-module.el @@ -54,6 +54,7 @@ eshell-modules-list eshell-basic eshell-cmpl eshell-dirs + eshell-extpipe eshell-glob eshell-hist eshell-ls diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el new file mode 100644 index 0000000000..01074c95c8 --- /dev/null +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -0,0 +1,122 @@ +;;; em-extpipe-tests.el --- em-extpipe test suite -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + + +;;; Code: + +(require 'cl-lib) +(require 'ert) +(require 'em-extpipe) +(load (expand-file-name "eshell-tests" + (file-name-directory (or load-file-name + default-directory)))) + +(declare-function with-temp-eshell "eshell-tests") + +(cl-macrolet + ((deftest (name input expected) + (let ((result (gensym))) + `(ert-deftest ,name () + (let* ((shell-file-name "sh") (shell-command-switch "-c") + (,result + (with-temp-eshell (eshell-parse-command ,input)))) + ;; Strip `eshell-trap-errors'. + (should (equal (cadr ,result) ,expected))))))) + + (deftest em-extpipe-test-1 + "foo *| bar >baz" + '(eshell-named-command "sh" + (list "-c" "foo | bar >baz"))) + + (deftest em-extpipe-test-2 + "foo | bar *>baz" + '(eshell-execute-pipeline + '((eshell-named-command "foo") + (eshell-named-command "sh" (list "-c" "bar >baz"))))) + + (deftest em-extpipe-test-3 + "foo *| bar | baz -d" + '(eshell-execute-pipeline + '((eshell-named-command "sh" (list "-c" "foo | bar")) + (eshell-named-command "baz" (list "-d"))))) + + (deftest em-extpipe-test-4 + "foo *| bar >#<buffer quux>" + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "quux"))) + (eshell-named-command "sh" + (list "-c" "foo | bar")))) + (deftest em-extpipe-test-5 + "foo *| bar >#<buffer quux> baz" + '(eshell-named-command + "sh" + (list "-c" "foo | bar >#<buffer quux> baz"))) + + (deftest em-extpipe-test-5 + "foo >#<buffer quux> *| bar baz" + '(eshell-named-command + "sh" + (list "-c" "foo >#<buffer quux> | bar baz"))) + + (deftest em-extpipe-test-7 + "foo *| bar >#<buffer quux> >>#<process other>" + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "quux"))) + (ignore + (eshell-set-output-handle 1 'append + (get-process "other"))) + (eshell-named-command "sh" + (list "-c" "foo | bar")))) + + (deftest em-extpipe-test-8 + "foo *| bar >/dev/kill | baz" + '(eshell-execute-pipeline + '((progn + (ignore + (eshell-set-output-handle 1 'overwrite "/dev/kill")) + (eshell-named-command "sh" + (list "-c" "foo | bar"))) + (eshell-named-command "baz")))) + + (deftest em-extpipe-test-9 + "foo \\*| bar" + '(eshell-execute-pipeline + '((eshell-named-command "foo" + (list (eshell-escape-arg "*"))) + (eshell-named-command "bar")))) + + (deftest em-extpipe-test-10 + "foo \"*|\" *>bar" + '(eshell-named-command "sh" + (list "-c" "foo \"*|\" >bar"))) + + (deftest em-extpipe-test-11 + "foo '*|' bar" + '(eshell-named-command + "foo" (list (eshell-escape-arg "*|") "bar")))) + +;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 5:19 ` Sean Whitton @ 2022-01-18 9:49 ` Robert Pluim 2022-01-18 18:27 ` Sean Whitton 2022-01-18 14:45 ` Eli Zaretskii ` (2 subsequent siblings) 3 siblings, 1 reply; 60+ messages in thread From: Robert Pluim @ 2022-01-18 9:49 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Michael Albinus >>>>> On Mon, 17 Jan 2022 22:19:59 -0700, Sean Whitton <spwhitton@spwhitton.name> said: Sean> Hello, Sean> On Tue 28 Dec 2021 at 09:58AM +01, Michael Albinus wrote: >> Sean Whitton <spwhitton@spwhitton.name> writes: >> >>> I'll see if I can implement this more complex thing and get back to you. >> >> Yes, please do. Sean> I've got it working. Please let me know what you think of the attached. How does this deal with something like: ls foo*|awk '{print $1}' Before your changes that expands 'foo*', but now the '*' gets swallowed by the '|'. Perhaps you should require whitespace before the '*'? Robert -- ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 9:49 ` Robert Pluim @ 2022-01-18 18:27 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-18 18:27 UTC (permalink / raw) To: Robert Pluim; +Cc: 46351, Michael Albinus On Tue 18 Jan 2022 at 10:49AM +01, Robert Pluim wrote: > How does this deal with something like: > > ls foo*|awk '{print $1}' > > Before your changes that expands 'foo*', but now the '*' gets > swallowed by the '|'. Perhaps you should require whitespace before the > '*'? Good catch. I will require whitespace in the next version. It should be hard to use this new functionality by accident. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 5:19 ` Sean Whitton 2022-01-18 9:49 ` Robert Pluim @ 2022-01-18 14:45 ` Eli Zaretskii 2022-01-18 18:40 ` Sean Whitton 2022-01-18 18:42 ` Sean Whitton 2022-01-19 15:52 ` Michael Albinus 2022-01-23 22:39 ` Sean Whitton 3 siblings, 2 replies; 60+ messages in thread From: Eli Zaretskii @ 2022-01-18 14:45 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Date: Mon, 17 Jan 2022 22:19:59 -0700 > > I've got it working. Please let me know what you think of the attached. Thanks. I see you play some games with shell syntactic rules and such: IME, these are often tricky enough to defeat simplistic handling. For example, does this support commands that have redirection operators _before_ the first verb? It's allowed in every shell I've seen, albeit very rarely used. I think Robert found another possible issue. > +(cl-macrolet > + ((deftest (name input expected) > + (let ((result (gensym))) > + `(ert-deftest ,name () > + (let* ((shell-file-name "sh") (shell-command-switch "-c") > + (,result > + (with-temp-eshell (eshell-parse-command ,input)))) > + ;; Strip `eshell-trap-errors'. > + (should (equal (cadr ,result) ,expected))))))) > + > + (deftest em-extpipe-test-1 > + "foo *| bar >baz" > + '(eshell-named-command "sh" > + (list "-c" "foo | bar >baz"))) Why do these tests only look for 'sh' as the shell? What is the importance of the shell for this purpose? ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 14:45 ` Eli Zaretskii @ 2022-01-18 18:40 ` Sean Whitton 2022-01-18 19:38 ` Eli Zaretskii 2022-01-18 18:42 ` Sean Whitton 1 sibling, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-18 18:40 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Tue 18 Jan 2022 at 04:45PM +02, Eli Zaretskii wrote: > I see you play some games with shell syntactic rules and such: IME, > these are often tricky enough to defeat simplistic handling. For > example, does this support commands that have redirection operators > _before_ the first verb? It's allowed in every shell I've seen, > albeit very rarely used. These things are indeed tricky. So long as it is not too easy to activate the functionality by accident, however, I think that it will be very helpful with regard to one of the most common reasons people cite for not using Eshell very much. > I think Robert found another possible issue. I will incorporate his suggestion to require whitespace. >> +(cl-macrolet >> + ((deftest (name input expected) >> + (let ((result (gensym))) >> + `(ert-deftest ,name () >> + (let* ((shell-file-name "sh") (shell-command-switch "-c") >> + (,result >> + (with-temp-eshell (eshell-parse-command ,input)))) >> + ;; Strip `eshell-trap-errors'. >> + (should (equal (cadr ,result) ,expected))))))) >> + >> + (deftest em-extpipe-test-1 >> + "foo *| bar >baz" >> + '(eshell-named-command "sh" >> + (list "-c" "foo | bar >baz"))) > > Why do these tests only look for 'sh' as the shell? What is the > importance of the shell for this purpose? The code just substitutes in the value of `shell-file-name' (taking TRAMP into account), so there isn't much point in varying that value in the tests, so I just picked something standard. The user is expected to know what syntax their external shell will accept and what it will do with it when they invoke this functionality. Thanks for looking -- please let me know if I have missed any of the points of your questions. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 18:40 ` Sean Whitton @ 2022-01-18 19:38 ` Eli Zaretskii 2022-01-18 23:16 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2022-01-18 19:38 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org > Date: Tue, 18 Jan 2022 11:40:50 -0700 > > > I see you play some games with shell syntactic rules and such: IME, > > these are often tricky enough to defeat simplistic handling. For > > example, does this support commands that have redirection operators > > _before_ the first verb? It's allowed in every shell I've seen, > > albeit very rarely used. > > These things are indeed tricky. So long as it is not too easy to > activate the functionality by accident, however, I think that it will be > very helpful with regard to one of the most common reasons people cite > for not using Eshell very much. OK, but I don't think I see how this answers my question. Is it possible today to say >foo something or other in Eshell, and have the output of 'something' redirected to 'foo'? If it is possible today, will it be possible after your changes, and what will happen with that if 'something' includes the new "*|" pipe symbol? > > Why do these tests only look for 'sh' as the shell? What is the > > importance of the shell for this purpose? > > The code just substitutes in the value of `shell-file-name' (taking > TRAMP into account), so there isn't much point in varying that value in > the tests, so I just picked something standard. The user is expected to > know what syntax their external shell will accept and what it will do > with it when they invoke this functionality. Does this mean that those tests will not run on systems where 'sh' is not available? ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 19:38 ` Eli Zaretskii @ 2022-01-18 23:16 ` Sean Whitton 2022-01-19 7:34 ` Eli Zaretskii 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-18 23:16 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Tue 18 Jan 2022 at 09:38PM +02, Eli Zaretskii wrote: > OK, but I don't think I see how this answers my question. Is it > possible today to say > > >foo something or other > > in Eshell, and have the output of 'something' redirected to 'foo'? If > it is possible today, will it be possible after your changes, and what > will happen with that if 'something' includes the new "*|" pipe > symbol? Interestingly, it's not possible to do that with plain Eshell. If someone modifies Eshell's parsing to make it possible, I do not believe it will interact with em-extpipe.el in any meaningful way. Even though >blah echo "Hello" doesn't work, with my patch, these three do, and all output to "blah": *>blah echo "Hello" echo "Hello" *| cat >blah echo "Hello" *| >blah cat If you do >blah echo "Hello" *| rev then on my system, "blah" contains "Hello", not "olleH", as it looks like GNU bash prioritises the output redirection over the pipe. To answer your question directly, then, if 'something' contains *| then the external shell will be asked to redirect the output of the first command to the file foo and also to pipe it to a second command, and I guess some shells might duplicate the output and others will just choose one destination; maybe POSIX has something to say about it. >> > Why do these tests only look for 'sh' as the shell? What is the >> > importance of the shell for this purpose? >> >> The code just substitutes in the value of `shell-file-name' (taking >> TRAMP into account), so there isn't much point in varying that value in >> the tests, so I just picked something standard. The user is expected to >> know what syntax their external shell will accept and what it will do >> with it when they invoke this functionality. > > Does this mean that those tests will not run on systems where 'sh' is > not available? No, they will run on any system that Emacs runs on. They test the parsing and rewriting done by em-extpipe.el. Actually executing commands is tested by test/lisp/eshell/eshell-tests.el. Executing the commands is not specific to em-extpipe.el. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 23:16 ` Sean Whitton @ 2022-01-19 7:34 ` Eli Zaretskii 2022-01-19 20:39 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2022-01-19 7:34 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org > Date: Tue, 18 Jan 2022 16:16:01 -0700 > > > >foo something or other > > > > in Eshell, and have the output of 'something' redirected to 'foo'? If > > it is possible today, will it be possible after your changes, and what > > will happen with that if 'something' includes the new "*|" pipe > > symbol? > > Interestingly, it's not possible to do that with plain Eshell. If > someone modifies Eshell's parsing to make it possible, I do not believe > it will interact with em-extpipe.el in any meaningful way. If it doesn't work now, then we cannot break it. > Even though > > >blah echo "Hello" > > doesn't work, with my patch, these three do, and all output to "blah": > > *>blah echo "Hello" > echo "Hello" *| cat >blah > echo "Hello" *| >blah cat These work because they delegate to the shell, and the shell does DTRT. > If you do > > >blah echo "Hello" *| rev > > then on my system, "blah" contains "Hello", not "olleH", as it looks > like GNU bash prioritises the output redirection over the pipe. Of course. If you want "olleH" in "blah", you need to do this instead: echo "Hello" *| >blah rev Thanks. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-19 7:34 ` Eli Zaretskii @ 2022-01-19 20:39 ` Sean Whitton 2022-01-20 6:53 ` Eli Zaretskii 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-19 20:39 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Wed 19 Jan 2022 at 09:34AM +02, Eli Zaretskii wrote: >> From: Sean Whitton <spwhitton@spwhitton.name> >> Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org >> Date: Tue, 18 Jan 2022 16:16:01 -0700 >> >> > >foo something or other >> > >> > in Eshell, and have the output of 'something' redirected to 'foo'? If >> > it is possible today, will it be possible after your changes, and what >> > will happen with that if 'something' includes the new "*|" pipe >> > symbol? >> >> Interestingly, it's not possible to do that with plain Eshell. If >> someone modifies Eshell's parsing to make it possible, I do not believe >> it will interact with em-extpipe.el in any meaningful way. > > If it doesn't work now, then we cannot break it. Okay, so I should add something to skip over my parsing code in the case that the first thing in the input is a redirection? -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-19 20:39 ` Sean Whitton @ 2022-01-20 6:53 ` Eli Zaretskii 2022-01-20 22:16 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2022-01-20 6:53 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org > Date: Wed, 19 Jan 2022 13:39:57 -0700 > > >> > >foo something or other > >> > > >> > in Eshell, and have the output of 'something' redirected to 'foo'? If > >> > it is possible today, will it be possible after your changes, and what > >> > will happen with that if 'something' includes the new "*|" pipe > >> > symbol? > >> > >> Interestingly, it's not possible to do that with plain Eshell. If > >> someone modifies Eshell's parsing to make it possible, I do not believe > >> it will interact with em-extpipe.el in any meaningful way. > > > > If it doesn't work now, then we cannot break it. > > Okay, so I should add something to skip over my parsing code in the case > that the first thing in the input is a redirection? I'm not sure I understand the intent: what do you want to accomplish by this special handling? ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-20 6:53 ` Eli Zaretskii @ 2022-01-20 22:16 ` Sean Whitton 2022-01-21 6:54 ` Eli Zaretskii 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-20 22:16 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Thu 20 Jan 2022 at 08:53AM +02, Eli Zaretskii wrote: >> From: Sean Whitton <spwhitton@spwhitton.name> >> Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org >> Date: Wed, 19 Jan 2022 13:39:57 -0700 >> >> >> > >foo something or other >> >> > >> >> > in Eshell, and have the output of 'something' redirected to 'foo'? If >> >> > it is possible today, will it be possible after your changes, and what >> >> > will happen with that if 'something' includes the new "*|" pipe >> >> > symbol? >> >> >> >> Interestingly, it's not possible to do that with plain Eshell. If >> >> someone modifies Eshell's parsing to make it possible, I do not believe >> >> it will interact with em-extpipe.el in any meaningful way. >> > >> > If it doesn't work now, then we cannot break it. >> >> Okay, so I should add something to skip over my parsing code in the case >> that the first thing in the input is a redirection? > > I'm not sure I understand the intent: what do you want to accomplish > by this special handling? Perhaps I misunderstood your earlier message. I thought that you were saying that because Eshell does not support redirections at the very beginning of the line at present, my new syntax should not support that either, to prevent confusion. Did you mean something else when you wrote "if it doesn't work now, then we cannot break it?" Thanks! -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-20 22:16 ` Sean Whitton @ 2022-01-21 6:54 ` Eli Zaretskii 2022-01-22 0:16 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Eli Zaretskii @ 2022-01-21 6:54 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, michael.albinus > From: Sean Whitton <spwhitton@spwhitton.name> > Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org > Date: Thu, 20 Jan 2022 15:16:20 -0700 > > >> Okay, so I should add something to skip over my parsing code in the case > >> that the first thing in the input is a redirection? > > > > I'm not sure I understand the intent: what do you want to accomplish > > by this special handling? > > Perhaps I misunderstood your earlier message. I thought that you were > saying that because Eshell does not support redirections at the very > beginning of the line at present, my new syntax should not support that > either, to prevent confusion. Did you mean something else when you > wrote "if it doesn't work now, then we cannot break it?" I meant that if Eshell doesn't support that syntax, you can do whatever you want in that case. You don't need to make any effort to support it, and you don't need to make any effort not to support it: whatever your code does in that case will be OK. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-21 6:54 ` Eli Zaretskii @ 2022-01-22 0:16 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-22 0:16 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Fri 21 Jan 2022 at 08:54AM +02, Eli Zaretskii wrote: >> From: Sean Whitton <spwhitton@spwhitton.name> >> Cc: michael.albinus@gmx.de, 46351@debbugs.gnu.org >> Date: Thu, 20 Jan 2022 15:16:20 -0700 >> >> >> Okay, so I should add something to skip over my parsing code in the case >> >> that the first thing in the input is a redirection? >> > >> > I'm not sure I understand the intent: what do you want to accomplish >> > by this special handling? >> >> Perhaps I misunderstood your earlier message. I thought that you were >> saying that because Eshell does not support redirections at the very >> beginning of the line at present, my new syntax should not support that >> either, to prevent confusion. Did you mean something else when you >> wrote "if it doesn't work now, then we cannot break it?" > > I meant that if Eshell doesn't support that syntax, you can do > whatever you want in that case. You don't need to make any effort to > support it, and you don't need to make any effort not to support it: > whatever your code does in that case will be OK. Ah, okay, I see now. Thanks. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 14:45 ` Eli Zaretskii 2022-01-18 18:40 ` Sean Whitton @ 2022-01-18 18:42 ` Sean Whitton 1 sibling, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-18 18:42 UTC (permalink / raw) To: Eli Zaretskii; +Cc: 46351, michael.albinus Hello, On Tue 18 Jan 2022 at 04:45PM +02, Eli Zaretskii wrote: > I see you play some games with shell syntactic rules and such: IME, > these are often tricky enough to defeat simplistic handling. For > example, does this support commands that have redirection operators > _before_ the first verb? It's allowed in every shell I've seen, > albeit very rarely used. Sorry, missed something -- redirection operators before the first verb work correctly. I will add a test to ensure that remains the case. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 5:19 ` Sean Whitton 2022-01-18 9:49 ` Robert Pluim 2022-01-18 14:45 ` Eli Zaretskii @ 2022-01-19 15:52 ` Michael Albinus 2022-01-19 20:54 ` Sean Whitton 2022-01-23 22:39 ` Sean Whitton 3 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2022-01-19 15:52 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello, Hi Sean, > I've got it working. Please let me know what you think of the attached. Some comments: > --- /dev/null > +++ b/test/lisp/eshell/em-extpipe-tests.el > @@ -0,0 +1,122 @@ > +(load (expand-file-name "eshell-tests" > + (file-name-directory (or load-file-name > + default-directory)))) This is problematic. Loading eshell-tests.el declares also all ert tests which are contained in that file. Running em-extpipe-tests.el in batch would run also all tests from that file, which is not intended I believe. A better approach would be to factor out the helper functions from eshell-tests.el into an own file, and load it here and in eshell-tests.el. > +(cl-macrolet > + ((deftest (name input expected) > + (let ((result (gensym))) > + `(ert-deftest ,name () > + (let* ((shell-file-name "sh") (shell-command-switch "-c") I'm not sure this is the right approach. Why do you change shell-file-name and shell-command-switch? You've spoken in another message about Tramp, but I don't understand this. Tramp has its own machinery to handle them, via connection-local variables. > + (deftest em-extpipe-test-7 Looks like em-extpipe-test-6 is missing. > Sean Whitton Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-19 15:52 ` Michael Albinus @ 2022-01-19 20:54 ` Sean Whitton 2022-01-20 18:41 ` Michael Albinus 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-19 20:54 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 Hello Michael, On Wed 19 Jan 2022 at 04:52PM +01, Michael Albinus wrote: >> --- /dev/null >> +++ b/test/lisp/eshell/em-extpipe-tests.el >> @@ -0,0 +1,122 @@ >> +(load (expand-file-name "eshell-tests" >> + (file-name-directory (or load-file-name >> + default-directory)))) > > This is problematic. Loading eshell-tests.el declares also all ert tests > which are contained in that file. Running em-extpipe-tests.el in batch > would run also all tests from that file, which is not intended I believe. > > A better approach would be to factor out the helper functions from > eshell-tests.el into an own file, and load it here and in eshell-tests.el. Good point, I'll factor that out. >> +(cl-macrolet >> + ((deftest (name input expected) >> + (let ((result (gensym))) >> + `(ert-deftest ,name () >> + (let* ((shell-file-name "sh") (shell-command-switch "-c") > > I'm not sure this is the right approach. Why do you change > shell-file-name and shell-command-switch? You've spoken in another > message about Tramp, but I don't understand this. Tramp has its own > machinery to handle them, via connection-local variables. The unit tests are all about seeing whether em-extpipe sets up `eshell-parse-command' to do the right thing. When it comes to shell-file-name and shell-command-switch, however, all em-extpipe does is substitute them in verbatim, using `with-connection-local-variables'. So there isn't much point in varying the values of the two variables in the tests. However, as the values of the two variables show up in the expected return values of `eshell-parse-command' that are part of the test definitions, in order to write the tests, I need to know what those two values will be. It seemed simplest just to bind them to constants. I could instead substitute the actual values of those variables into the expected return values. It seems to me that would sacrifice readability of the tests, though. Am I perhaps missing some other benefit? >> + (deftest em-extpipe-test-7 > > Looks like em-extpipe-test-6 is missing. Oops, will fix. Many thanks for the review. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-19 20:54 ` Sean Whitton @ 2022-01-20 18:41 ` Michael Albinus 2022-01-20 22:17 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2022-01-20 18:41 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello Michael, Hi Sean, > I could instead substitute the actual values of those variables into the > expected return values. It seems to me that would sacrifice readability > of the tests, though. Am I perhaps missing some other benefit? I understand your intentions, they are OK. However, if I understand your test cases, they check that the eshell commands are manipulated as you expect. The tests do not run the resulting command itself, checking the output. This is a little bit unfortune, because you could check that the output is indeed what you expect. And perhaps you could find some constellations, where the output is different when using either *| or |. This would be another proof that your changes work. And this would also give some guidance, where your approach has limitations (if exist). Showing also *failing* tests in one way or another is always a benefit. Your tests use only should, there is no should-not or should-error. Another benefit of testing working examples and their output is documentation. I always have trouble to understand documentation of unknown (to me) features in Emacs. I have the attitude to run the respective ert tests, and to study how the feature is applied by its developer. Often, it is more instructive than documentation, and I can steal the code. > Many thanks for the review. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-20 18:41 ` Michael Albinus @ 2022-01-20 22:17 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-20 22:17 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351 Hello, On Thu 20 Jan 2022 at 07:41PM +01, Michael Albinus wrote: > However, if I understand your test cases, they check that the eshell > commands are manipulated as you expect. The tests do not run the > resulting command itself, checking the output. This is a little bit > unfortune, because you could check that the output is indeed what you > expect. And perhaps you could find some constellations, where the output > is different when using either *| or |. This would be another proof that > your changes work. > > And this would also give some guidance, where your approach has > limitations (if exist). Showing also *failing* tests in one way or > another is always a benefit. Your tests use only should, there is no > should-not or should-error. > > Another benefit of testing working examples and their output is > documentation. I always have trouble to understand documentation of > unknown (to me) features in Emacs. I have the attitude to run the > respective ert tests, and to study how the feature is applied by its > developer. Often, it is more instructive than documentation, and I can > steal the code. I hadn't thought of tests as specifically useful alongside documentation. Thank you. I will include some additional tests of this nature in my next revision. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-18 5:19 ` Sean Whitton ` (2 preceding siblings ...) 2022-01-19 15:52 ` Michael Albinus @ 2022-01-23 22:39 ` Sean Whitton 2022-01-24 9:55 ` Lars Ingebrigtsen 2022-01-24 14:18 ` Michael Albinus 3 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-23 22:39 UTC (permalink / raw) To: Michael Albinus, 46351; +Cc: Robert Pluim [-- Attachment #1: Type: text/plain, Size: 1366 bytes --] Hello, On Mon 17 Jan 2022 at 10:19PM -07, Sean Whitton wrote: > I've got it working. Please let me know what you think of the attached. Attached is a revised series addressing feedback gratefully received over the past week. On Thu 20 Jan 2022 at 07:41PM +01, Michael Albinus wrote: > However, if I understand your test cases, they check that the eshell > commands are manipulated as you expect. The tests do not run the > resulting command itself, checking the output. This is a little bit > unfortune, because you could check that the output is indeed what you > expect. And perhaps you could find some constellations, where the output > is different when using either *| or |. This would be another proof that > your changes work. > > And this would also give some guidance, where your approach has > limitations (if exist). Showing also *failing* tests in one way or > another is always a benefit. Your tests use only should, there is no > should-not or should-error. I've added actually running the commands and examining the results to several of the tests. I found a way to show different output in the case of *| vs. | by using cl-letf to redefine some Lisp functions. I also refactored the tests in the hope of increasing their value as a supplement to the documentation. Please let me know if you have any other ideas. Thanks! -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v4-0001-Move-Eshell-test-helpers-to-their-own-file.patch --] [-- Type: text/x-patch, Size: 6953 bytes --] From 1cfae74bbf78093c84bebaa6dc97bc887238287c Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Fri, 21 Jan 2022 22:32:22 -0700 Subject: [PATCH v4 1/3] Move Eshell test helpers to their own file * test/lisp/eshell/eshell-tests.el: * test/lisp/eshell/eshell-tests-helpers.el: Move helpers to own file. --- test/lisp/eshell/eshell-tests-helpers.el | 90 ++++++++++++++++++++++++ test/lisp/eshell/eshell-tests.el | 61 +++------------- 2 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 test/lisp/eshell/eshell-tests-helpers.el diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el new file mode 100644 index 0000000000..2afa63ae51 --- /dev/null +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -0,0 +1,90 @@ +;;; eshell-tests-helpers.el --- Eshell test suite helpers -*- lexical-binding:t -*- + +;; Copyright (C) 1999-2022 Free Software Foundation, Inc. + +;; Author: John Wiegley <johnw@gnu.org> + +;; 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: + +;; Eshell test suite helpers. + +;;; Code: + +(require 'ert) +(require 'ert-x) +(require 'esh-mode) +(require 'eshell) + +(defvar eshell-test--max-subprocess-time 5 + "The maximum amount of time to wait for a subprocess to finish, in seconds. +See `eshell-wait-for-subprocess'.") + +(defmacro with-temp-eshell (&rest body) + "Evaluate BODY in a temporary Eshell buffer." + `(ert-with-temp-directory eshell-directory-name + (let* (;; We want no history file, so prevent Eshell from falling + ;; back on $HISTFILE. + (process-environment (cons "HISTFILE" process-environment)) + (eshell-history-file-name nil) + (eshell-buffer (eshell t))) + (unwind-protect + (with-current-buffer eshell-buffer + ,@body) + (let (kill-buffer-query-functions) + (kill-buffer eshell-buffer)))))) + +(defun eshell-wait-for-subprocess () + "Wait until there is no interactive subprocess running in Eshell. +If this takes longer than `eshell-test--max-subprocess-time', +raise an error." + (let ((start (current-time))) + (while (eshell-interactive-process) + (when (> (float-time (time-since start)) + eshell-test--max-subprocess-time) + (error "timed out waiting for subprocess")) + (sit-for 0.1)))) + +(defun eshell-insert-command (text &optional func) + "Insert a command at the end of the buffer." + (goto-char eshell-last-output-end) + (insert-and-inherit text) + (funcall (or func 'eshell-send-input))) + +(defun eshell-match-result (regexp) + "Check that text after `eshell-last-input-end' matches REGEXP." + (goto-char eshell-last-input-end) + (should (string-match-p regexp (buffer-substring-no-properties + (point) (point-max))))) + +(defun eshell-command-result-p (text regexp &optional func) + "Insert a command at the end of the buffer." + (eshell-insert-command text func) + (eshell-wait-for-subprocess) + (eshell-match-result regexp)) + +(defvar eshell-history-file-name) + +(defun eshell-test-command-result (command) + "Like `eshell-command-result', but not using HOME." + (ert-with-temp-directory eshell-directory-name + (let ((eshell-history-file-name nil)) + (eshell-command-result command)))) + +(provide 'eshell-tests) + +;;; eshell-tests.el ends here diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 1a7ab0ab06..6aeefdfde2 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -29,61 +29,16 @@ (require 'ert-x) (require 'esh-mode) (require 'eshell) - -(defvar eshell-test--max-subprocess-time 5 - "The maximum amount of time to wait for a subprocess to finish, in seconds. -See `eshell-wait-for-subprocess'.") - -(defmacro with-temp-eshell (&rest body) - "Evaluate BODY in a temporary Eshell buffer." - `(ert-with-temp-directory eshell-directory-name - (let* (;; We want no history file, so prevent Eshell from falling - ;; back on $HISTFILE. - (process-environment (cons "HISTFILE" process-environment)) - (eshell-history-file-name nil) - (eshell-buffer (eshell t))) - (unwind-protect - (with-current-buffer eshell-buffer - ,@body) - (let (kill-buffer-query-functions) - (kill-buffer eshell-buffer)))))) - -(defun eshell-wait-for-subprocess () - "Wait until there is no interactive subprocess running in Eshell. -If this takes longer than `eshell-test--max-subprocess-time', -raise an error." - (let ((start (current-time))) - (while (eshell-interactive-process) - (when (> (float-time (time-since start)) - eshell-test--max-subprocess-time) - (error "timed out waiting for subprocess")) - (sit-for 0.1)))) - -(defun eshell-insert-command (text &optional func) - "Insert a command at the end of the buffer." - (goto-char eshell-last-output-end) - (insert-and-inherit text) - (funcall (or func 'eshell-send-input))) - -(defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) - -(defun eshell-command-result-p (text regexp &optional func) - "Insert a command at the end of the buffer." - (eshell-insert-command text func) - (eshell-wait-for-subprocess) - (eshell-match-result regexp)) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) (defvar eshell-history-file-name) - -(defun eshell-test-command-result (command) - "Like `eshell-command-result', but not using HOME." - (ert-with-temp-directory eshell-directory-name - (let ((eshell-history-file-name nil)) - (eshell-command-result command)))) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-insert-command "eshell-tests-helpers") +(declare-function eshell-match-result "eshell-tests-helpers") +(declare-function eshell-command-result-p "eshell-tests-helpers") ;;; Tests: -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #3: v4-0002-Rework-eshell-match-result-for-testing-asynchrono.patch --] [-- Type: text/x-patch, Size: 2634 bytes --] From 7d8dc8906973371fe5ce38f676bba39d57af17cc Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 22 Jan 2022 18:54:55 -0700 Subject: [PATCH v4 2/3] Rework eshell-match-result for testing asynchronous commands When using eshell-match-result via eshell-command-result-p to examine the output of asynchronous Eshell commands, a newly emitted prompt is included in the text against which the regexp is matched. This makes it awkward to match against the whole output; for example, to check whether it is empty. Rework the function to exclude the prompt. * test/lisp/eshell/eshell-tests-helpers.el (eshell-match-result): Exclude any newly emitted prompt from the text against which the regexp is matched. Additionally, the function no longer moves point. * test/lisp/eshell/eshell-tests.el (eshell-test/flush-output): Update and simplify test given how eshell-match-result no longer moves point. --- test/lisp/eshell/eshell-tests-helpers.el | 9 +++++---- test/lisp/eshell/eshell-tests.el | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el index 2afa63ae51..a150adb144 100644 --- a/test/lisp/eshell/eshell-tests-helpers.el +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -66,10 +66,11 @@ eshell-insert-command (funcall (or func 'eshell-send-input))) (defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) + "Check that output of last command matches REGEXP." + (should + (string-match-p + regexp (buffer-substring-no-properties + (eshell-beginning-of-output) (eshell-end-of-output))))) (defun eshell-command-result-p (text regexp &optional func) "Insert a command at the end of the buffer." diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 6aeefdfde2..542815df80 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -232,9 +232,8 @@ eshell-test/flush-output (with-temp-eshell (eshell-insert-command "echo alpha") (eshell-kill-output) - (eshell-match-result (regexp-quote "*** output flushed ***\n")) - (should (forward-line)) - (should (= (point) eshell-last-output-start)))) + (eshell-match-result + (concat "^" (regexp-quote "*** output flushed ***\n") "$")))) (ert-deftest eshell-test/run-old-command () "Re-run an old command" -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #4: v4-0003-Add-Eshell-syntax-to-more-easily-bypass-Eshell-s-.patch --] [-- Type: text/x-patch, Size: 20218 bytes --] From f12e596aa3112397a26ad9675ca1de091f04c140 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 17 Jan 2022 15:15:36 -0700 Subject: [PATCH v4 3/3] Add Eshell syntax to more easily bypass Eshell's own pipelining * etc/NEWS: * doc/misc/eshell.texi (Input/Output): Document the new syntax. * lisp/eshell/em-extpipe.el: New module. * test/lisp/eshell/em-extpipe-tests.el: New tests. * lisp/eshell/esh-module.el (eshell-modules-list): Add `eshell-extpipe'. --- doc/misc/eshell.texi | 42 ++++++ etc/NEWS | 10 ++ lisp/eshell/em-extpipe.el | 183 ++++++++++++++++++++++++ lisp/eshell/esh-module.el | 1 + test/lisp/eshell/em-extpipe-tests.el | 205 +++++++++++++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 lisp/eshell/em-extpipe.el create mode 100644 test/lisp/eshell/em-extpipe-tests.el diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index df6e3b861e..261e88d00c 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1142,6 +1142,48 @@ Input/Output 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 +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 +executing commands on a remote machine using Eshell's Tramp +integration: using the remote shell's pipelining avoids copying the +data which will flow through the pipeline to local Emacs buffers and +then right back again. + +Eshell recognises a special syntax to make it easier to convert +pipelines so as to bypass Eshell's pipelining. Prefixing at least one +@code{|}, @code{<} or @code{>} with an asterisk marks a command as +intended for the operating system shell. To make it harder to invoke +this functionality accidentally, it is also required that the asterisk +be preceded by whitespace or located at the start of input. For +example, + +@example + cat *.ogg *| my-cool-decoder >file +@end example + +Executing this command will not copy all the data in the *.ogg files, +nor the decoded data, into Emacs buffers, as would normally happen. + +The command is interpreted as extending up to the next @code{|} +character which is not preceded by an unescaped asterisk following +whitespace, or the end of the input if there is no such character. +Thus, all @code{<} and @code{>} redirections occuring before the next +asterisk-unprefixed @code{|} are implicitly prefixed with (whitespace +and) asterisks. An exception is that Eshell-specific redirects right +at the end of the command are excluded. This allows input like this: + +@example + foo *| baz >#<buffer quux> +@end example + +@noindent which is equivalent to input like this: + +@example + sh -c "foo | baz" >#<buffer quux> +@end example + @node Extension modules @chapter Extension modules Eshell provides a facility for defining extension modules so that they diff --git a/etc/NEWS b/etc/NEWS index 5297db3e2d..68c0eba866 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -858,6 +858,16 @@ the Galeon web browser was released in September, 2008. *** New user option 'ruby-toggle-block-space-before-parameters'. +** Eshell + ++++ +*** New feature to easily bypass Eshell's own pipelining. +Prefixing '|', '<' or '>' with an asterisk, i.e. '*|', '*<' or '*>', +will cause the whole command to be passed to the operating system +shell. This is particularly useful to bypass Eshell's own pipelining +support for pipelines which will move a lot of data. See "Running +Shell Pipelines Natively" in the Eshell manual. + ** Miscellaneous --- diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el new file mode 100644 index 0000000000..57aeec38ff --- /dev/null +++ b/lisp/eshell/em-extpipe.el @@ -0,0 +1,183 @@ +;;; em-extpipe.el --- external shell pipelines -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + +;; 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 module tries to make +;; that easy to do. + +;;; Code: + +(require 'cl-lib) +(require 'esh-arg) +(require 'esh-io) +(require 'esh-util) + +(eval-when-compile (require 'files-x)) + +;;; Functions: + +(defun eshell-extpipe-initialize () ;Called from `eshell-mode' via intern-soft! + "Initialize external pipelines support." + (when (boundp 'eshell-special-chars-outside-quoting) + (setq-local + eshell-special-chars-outside-quoting + (append eshell-special-chars-outside-quoting (list ?\*)))) + (add-hook 'eshell-parse-argument-hook + #'eshell-parse-external-pipeline -20 t) + (add-hook 'eshell-pre-rewrite-command-hook + #'eshell-rewrite-external-pipeline -20 t)) + +(defun eshell-parse-external-pipeline () + "Parse a pipeline intended for execution by the external shell. + +A sequence of arguments is rewritten to use the operating system +shell when it contains `*|', `*<' or `*>', where the asterisk is +preceded by whitespace or located at the start of input. + +The command extends to the next `|' character which is not +preceded by an unescaped asterisk following whitespace, or the +end of input, except that any Eshell-specific output redirections +occurring at the end are excluded. Any other `<' or `>' +appearing before the end of the command are treated as though +preceded by (whitespace and) an asterisk. + +For example, + + foo <bar *| baz >#<buffer quux> + +is equivalent to + + sh -c \"foo <bar | baz\" >#<buffer quux> + +when `shell-file-name' is `sh' and `shell-command-switch' is +`-c', but in + + foo >#<buffer quux> *| baz + +and + + foo *| baz >#<buffer quux> --some-argument + +the Eshell-specific redirect will be passed on to the operating +system shell, probably leading to undesired results. + +This function must appear early in `eshell-parse-argument-hook' +to ensure that operating system shell syntax is not interpreted +as though it were Eshell syntax." + ;; Our goal is to wrap the external command to protect it from the + ;; other members of `eshell-parse-argument-hook'. We must avoid + ;; misinterpreting a quoted `*|', `*<' or `*>' as indicating an + ;; external pipeline, hence the structure of the loop in `findbeg1'. + (cl-flet + ((findbeg1 (pat &optional go (bound (point-max))) + (let* ((start (point)) + (result + (catch 'found + (while (> bound (point)) + (let* ((found + (save-excursion + (re-search-forward "['\"\\]" bound t))) + (next (or (and found (match-beginning 0)) + bound))) + (if (re-search-forward pat next t) + (throw 'found (match-beginning 1)) + (goto-char next) + (while (or (eshell-parse-backslash) + (eshell-parse-double-quote) + (eshell-parse-literal-quote))))))))) + (goto-char (if (and result go) (match-end 0) start)) + result))) + (unless (or eshell-current-argument eshell-current-quoted) + (let ((beg (point)) end + (next-marked (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)")) + (next-unmarked + (or (findbeg1 "\\(?:\\=\\|[^*]\\|\\S-\\*\\)\\(|\\)") + (point-max)))) + (when (and next-marked (> next-unmarked next-marked) + (or (> next-marked (point)) + (looking-back "\\`\\|\\s-" nil))) + ;; Skip to the final segment of the external pipeline. + (while (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*|\\)" t)) + ;; Find output redirections. + (while (findbeg1 + "\\([0-9]?>+&?[0-9]?\\s-*\\S-\\)" t next-unmarked) + ;; Is the output redirection Eshell-specific? We have our + ;; own logic, rather than calling `eshell-parse-argument', + ;; to avoid specifying here all the possible cars of + ;; parsed special references -- `get-buffer-create' etc. + (forward-char -1) + (let ((this-end + (save-match-data + (cond ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0)))))) + (cond ((and this-end end) + (goto-char this-end)) + (this-end + (goto-char this-end) + (setq end (match-beginning 0))) + (t + (setq end nil))))) + ;; We've moved past all Eshell-specific output redirections + ;; we could find. If there is only whitespace left, then + ;; `end' is right before redirections we should exclude; + ;; otherwise, we must include everything. + (unless (and end (skip-syntax-forward "\s" next-unmarked) + (= next-unmarked (point))) + (setq end next-unmarked)) + (let ((cmd (string-trim + (buffer-substring-no-properties beg end)))) + (goto-char end) + ;; We must now drop the asterisks, unless quoted/escaped. + (with-temp-buffer + (insert cmd) + (goto-char (point-min)) + (cl-loop + for next = (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)" t) + while next do (forward-char -2) (delete-char 1)) + (eshell-finish-arg + `(eshell-external-pipeline ,(buffer-string)))))))))) + +(defun eshell-rewrite-external-pipeline (terms) + "Rewrite an external pipeline in TERMS as parsed by +`eshell-parse-external-pipeline', which see." + (while terms + (when (and (listp (car terms)) + (eq (caar terms) 'eshell-external-pipeline)) + (with-connection-local-variables + (setcdr terms (cl-list* + shell-command-switch (cadar terms) (cdr terms))) + (setcar terms shell-file-name))) + (setq terms (cdr terms)))) + +(defsubst eshell-external-pipeline (&rest _args) + "Stub to generate an error if a pipeline is not rewritten." + (error "Unhandled external pipeline in input text")) + +(provide 'em-extpipe) +;;; esh-extpipe.el ends here diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el index ade151d7cd..14e91912d1 100644 --- a/lisp/eshell/esh-module.el +++ b/lisp/eshell/esh-module.el @@ -54,6 +54,7 @@ eshell-modules-list eshell-basic eshell-cmpl eshell-dirs + eshell-extpipe eshell-glob eshell-hist eshell-ls diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el new file mode 100644 index 0000000000..2c3f11522b --- /dev/null +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -0,0 +1,205 @@ +;;; em-extpipe-tests.el --- em-extpipe test suite -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + + +;;; Code: + +(require 'cl-lib) +(require 'ert) +(require 'ert-x) +(require 'em-extpipe) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) + +(defvar eshell-history-file-name) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-command-result-p "eshell-tests-helpers") + +(defmacro em-extpipe-tests--deftest (name input &rest body) + (declare (indent 2)) + `(ert-deftest ,name () + (cl-macrolet + ((should-parse (expected) + `(let ((shell-file-name "sh") + (shell-command-switch "-c")) + ;; Strip `eshell-trap-errors'. + (should (equal ,expected + (cadr (eshell-parse-command input)))))) + (with-substitute-for-temp (&rest body) + ;; Substitute name of an actual temporary file and/or + ;; buffer into `input'. The substitution logic is + ;; appropriate for only the use we put it to in this file. + `(ert-with-temp-file temp + (let ((temp-buffer (generate-new-buffer " *temp*" t))) + (unwind-protect + (let ((input + (replace-regexp-in-string + "temp\\([^>]\\|\\'\\)" temp + (string-replace "#<buffer temp>" + (buffer-name temp-buffer) + input)))) + ,@body) + (when (buffer-name temp-buffer) + (kill-buffer temp-buffer)))))) + (temp-should-string= (expected) + `(string= ,expected (string-trim-right + (with-temp-buffer + (insert-file-contents temp) + (buffer-string))))) + (temp-buffer-should-string= (expected) + `(string= ,expected (string-trim-right + (with-current-buffer temp-buffer + (buffer-string)))))) + (skip-unless shell-file-name) + (skip-unless shell-command-switch) + (skip-unless (executable-find shell-file-name)) + (let ((input ,input)) + (with-temp-eshell ,@body))))) + +(em-extpipe-tests--deftest em-extpipe-test-1 + "echo \"bar\" *| rev >temp" + (skip-unless (executable-find "rev")) + (should-parse '(eshell-named-command + "sh" (list "-c" "echo \"bar\" | rev >temp"))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-2 + "echo \"bar\" | rev *>temp" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "echo" (list (eshell-escape-arg "bar"))) + (eshell-named-command "sh" (list "-c" "rev >temp"))))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-3 "foo *| bar | baz -d" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "sh" (list "-c" "foo | bar")) + (eshell-named-command "baz" (list "-d")))))) + +(em-extpipe-tests--deftest em-extpipe-test-4 + "echo \"bar\" *| rev >#<buffer temp>" + (skip-unless (executable-find "rev")) + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "temp"))) + (eshell-named-command "sh" + (list "-c" "echo \"bar\" | rev")))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-buffer-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-5 + "foo *| bar >#<buffer quux> baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo | bar >#<buffer quux> baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-6 + "foo >#<buffer quux> *| bar baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo >#<buffer quux> | bar baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-7 + "foo *| bar >#<buffer quux> >>#<process other>" + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "quux"))) + (ignore + (eshell-set-output-handle 1 'append + (get-process "other"))) + (eshell-named-command "sh" + (list "-c" "foo | bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-8 + "foo *| bar >/dev/kill | baz" + (should-parse + '(eshell-execute-pipeline + '((progn + (ignore + (eshell-set-output-handle 1 'overwrite "/dev/kill")) + (eshell-named-command "sh" + (list "-c" "foo | bar"))) + (eshell-named-command "baz"))))) + +(em-extpipe-tests--deftest em-extpipe-test-9 "foo \\*| bar" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "foo" + (list (eshell-escape-arg "*"))) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-10 "foo \"*|\" *>bar" + (should-parse + '(eshell-named-command "sh" (list "-c" "foo \"*|\" >bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-11 "foo '*|' bar" + (should-parse '(eshell-named-command + "foo" (list (eshell-escape-arg "*|") "bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-12 ">foo bar *| baz" + (should-parse + '(eshell-named-command "sh" (list "-c" ">foo bar | baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-13 "foo*|bar" + (should-parse '(eshell-execute-pipeline + '((eshell-named-command (concat "foo" "*")) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-14 "tac *<temp" + (skip-unless (executable-find "tac")) + (should-parse '(eshell-named-command "sh" (list "-c" "tac <temp"))) + (with-substitute-for-temp + (with-temp-buffer (insert "bar\nbaz") (write-file temp)) + (eshell-command-result-p input "baz\nbar"))) + +(em-extpipe-tests--deftest em-extpipe-test-15 "echo \"bar\" *| cat" + (skip-unless (executable-find "cat")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | cat"))) + (cl-letf (((symbol-function 'eshell/cat) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "bar") + (eshell-command-result-p "echo \"bar\" | cat" "nonsense"))) + +(em-extpipe-tests--deftest em-extpipe-test-16 "echo \"bar\" *| rev" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | rev"))) + (let ((eshell-prefer-lisp-functions t)) + (cl-letf (((symbol-function 'rev) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "rab") + (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) + +;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-23 22:39 ` Sean Whitton @ 2022-01-24 9:55 ` Lars Ingebrigtsen 2022-01-24 14:18 ` Michael Albinus 1 sibling, 0 replies; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-24 9:55 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Robert Pluim, Michael Albinus Sean Whitton <spwhitton@spwhitton.name> writes: > I've added actually running the commands and examining the results to > several of the tests. I found a way to show different output in the > case of *| vs. | by using cl-letf to redefine some Lisp functions. This leads to: Test em-extpipe-test-14 condition: (ert-test-failed ((should (string-match-p regexp (buffer-substring-no-properties ... ...))) :form (string-match-p "baz\nbar" "bazbar\n") :value nil)) FAILED 6/16 em-extpipe-test-14 (0.119788 sec) -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-23 22:39 ` Sean Whitton 2022-01-24 9:55 ` Lars Ingebrigtsen @ 2022-01-24 14:18 ` Michael Albinus 2022-01-24 20:32 ` Sean Whitton 1 sibling, 1 reply; 60+ messages in thread From: Michael Albinus @ 2022-01-24 14:18 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Robert Pluim Sean Whitton <spwhitton@spwhitton.name> writes: > Hello, Hi Sean, > I've added actually running the commands and examining the results to > several of the tests. I found a way to show different output in the > case of *| vs. | by using cl-letf to redefine some Lisp functions. > > I also refactored the tests in the hope of increasing their value as a > supplement to the documentation. Please let me know if you have any > other ideas. Thanks! Thanks. Besides the problem reported by Lars, just a minor comment: > --- /dev/null > +++ b/test/lisp/eshell/eshell-tests-helpers.el > @@ -0,0 +1,90 @@ > +(provide 'eshell-tests) > + > +;;; eshell-tests.el ends here This shall be eshell-tests-helpers in both cases. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 14:18 ` Michael Albinus @ 2022-01-24 20:32 ` Sean Whitton 2022-01-24 20:44 ` Sean Whitton 2022-01-24 20:46 ` Lars Ingebrigtsen 0 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-24 20:32 UTC (permalink / raw) To: Michael Albinus, Lars Ingebrigtsen; +Cc: 46351, Robert Pluim [-- Attachment #1: Type: text/plain, Size: 869 bytes --] Hello, On Mon 24 Jan 2022 at 10:55AM +01, Lars Ingebrigtsen wrote: > This leads to: > > Test em-extpipe-test-14 condition: > (ert-test-failed > ((should > (string-match-p regexp > (buffer-substring-no-properties ... ...))) > :form > (string-match-p "baz\nbar" "bazbar\n") > :value nil)) > FAILED 6/16 em-extpipe-test-14 (0.119788 sec) I apologies for neglecting to run the tests in batch mode. There was an implicit dependency on my setting for require-final-newline. Now fixed. On Mon 24 Jan 2022 at 03:18PM +01, Michael Albinus wrote: >> --- /dev/null >> +++ b/test/lisp/eshell/eshell-tests-helpers.el >> @@ -0,0 +1,90 @@ >> +(provide 'eshell-tests) >> + >> +;;; eshell-tests.el ends here > > This shall be eshell-tests-helpers in both cases. Likewise fixed in the attached. Thank you both. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v5-0001-Move-Eshell-test-helpers-to-their-own-file.patch --] [-- Type: text/x-patch, Size: 6953 bytes --] From 1cfae74bbf78093c84bebaa6dc97bc887238287c Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Fri, 21 Jan 2022 22:32:22 -0700 Subject: [PATCH v5 1/3] Move Eshell test helpers to their own file * test/lisp/eshell/eshell-tests.el: * test/lisp/eshell/eshell-tests-helpers.el: Move helpers to own file. --- test/lisp/eshell/eshell-tests-helpers.el | 90 ++++++++++++++++++++++++ test/lisp/eshell/eshell-tests.el | 61 +++------------- 2 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 test/lisp/eshell/eshell-tests-helpers.el diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el new file mode 100644 index 0000000000..2afa63ae51 --- /dev/null +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -0,0 +1,90 @@ +;;; eshell-tests-helpers.el --- Eshell test suite helpers -*- lexical-binding:t -*- + +;; Copyright (C) 1999-2022 Free Software Foundation, Inc. + +;; Author: John Wiegley <johnw@gnu.org> + +;; 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: + +;; Eshell test suite helpers. + +;;; Code: + +(require 'ert) +(require 'ert-x) +(require 'esh-mode) +(require 'eshell) + +(defvar eshell-test--max-subprocess-time 5 + "The maximum amount of time to wait for a subprocess to finish, in seconds. +See `eshell-wait-for-subprocess'.") + +(defmacro with-temp-eshell (&rest body) + "Evaluate BODY in a temporary Eshell buffer." + `(ert-with-temp-directory eshell-directory-name + (let* (;; We want no history file, so prevent Eshell from falling + ;; back on $HISTFILE. + (process-environment (cons "HISTFILE" process-environment)) + (eshell-history-file-name nil) + (eshell-buffer (eshell t))) + (unwind-protect + (with-current-buffer eshell-buffer + ,@body) + (let (kill-buffer-query-functions) + (kill-buffer eshell-buffer)))))) + +(defun eshell-wait-for-subprocess () + "Wait until there is no interactive subprocess running in Eshell. +If this takes longer than `eshell-test--max-subprocess-time', +raise an error." + (let ((start (current-time))) + (while (eshell-interactive-process) + (when (> (float-time (time-since start)) + eshell-test--max-subprocess-time) + (error "timed out waiting for subprocess")) + (sit-for 0.1)))) + +(defun eshell-insert-command (text &optional func) + "Insert a command at the end of the buffer." + (goto-char eshell-last-output-end) + (insert-and-inherit text) + (funcall (or func 'eshell-send-input))) + +(defun eshell-match-result (regexp) + "Check that text after `eshell-last-input-end' matches REGEXP." + (goto-char eshell-last-input-end) + (should (string-match-p regexp (buffer-substring-no-properties + (point) (point-max))))) + +(defun eshell-command-result-p (text regexp &optional func) + "Insert a command at the end of the buffer." + (eshell-insert-command text func) + (eshell-wait-for-subprocess) + (eshell-match-result regexp)) + +(defvar eshell-history-file-name) + +(defun eshell-test-command-result (command) + "Like `eshell-command-result', but not using HOME." + (ert-with-temp-directory eshell-directory-name + (let ((eshell-history-file-name nil)) + (eshell-command-result command)))) + +(provide 'eshell-tests) + +;;; eshell-tests.el ends here diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 1a7ab0ab06..6aeefdfde2 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -29,61 +29,16 @@ (require 'ert-x) (require 'esh-mode) (require 'eshell) - -(defvar eshell-test--max-subprocess-time 5 - "The maximum amount of time to wait for a subprocess to finish, in seconds. -See `eshell-wait-for-subprocess'.") - -(defmacro with-temp-eshell (&rest body) - "Evaluate BODY in a temporary Eshell buffer." - `(ert-with-temp-directory eshell-directory-name - (let* (;; We want no history file, so prevent Eshell from falling - ;; back on $HISTFILE. - (process-environment (cons "HISTFILE" process-environment)) - (eshell-history-file-name nil) - (eshell-buffer (eshell t))) - (unwind-protect - (with-current-buffer eshell-buffer - ,@body) - (let (kill-buffer-query-functions) - (kill-buffer eshell-buffer)))))) - -(defun eshell-wait-for-subprocess () - "Wait until there is no interactive subprocess running in Eshell. -If this takes longer than `eshell-test--max-subprocess-time', -raise an error." - (let ((start (current-time))) - (while (eshell-interactive-process) - (when (> (float-time (time-since start)) - eshell-test--max-subprocess-time) - (error "timed out waiting for subprocess")) - (sit-for 0.1)))) - -(defun eshell-insert-command (text &optional func) - "Insert a command at the end of the buffer." - (goto-char eshell-last-output-end) - (insert-and-inherit text) - (funcall (or func 'eshell-send-input))) - -(defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) - -(defun eshell-command-result-p (text regexp &optional func) - "Insert a command at the end of the buffer." - (eshell-insert-command text func) - (eshell-wait-for-subprocess) - (eshell-match-result regexp)) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) (defvar eshell-history-file-name) - -(defun eshell-test-command-result (command) - "Like `eshell-command-result', but not using HOME." - (ert-with-temp-directory eshell-directory-name - (let ((eshell-history-file-name nil)) - (eshell-command-result command)))) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-insert-command "eshell-tests-helpers") +(declare-function eshell-match-result "eshell-tests-helpers") +(declare-function eshell-command-result-p "eshell-tests-helpers") ;;; Tests: -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #3: v5-0002-Rework-eshell-match-result-for-testing-asynchrono.patch --] [-- Type: text/x-patch, Size: 2634 bytes --] From 7d8dc8906973371fe5ce38f676bba39d57af17cc Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 22 Jan 2022 18:54:55 -0700 Subject: [PATCH v5 2/3] Rework eshell-match-result for testing asynchronous commands When using eshell-match-result via eshell-command-result-p to examine the output of asynchronous Eshell commands, a newly emitted prompt is included in the text against which the regexp is matched. This makes it awkward to match against the whole output; for example, to check whether it is empty. Rework the function to exclude the prompt. * test/lisp/eshell/eshell-tests-helpers.el (eshell-match-result): Exclude any newly emitted prompt from the text against which the regexp is matched. Additionally, the function no longer moves point. * test/lisp/eshell/eshell-tests.el (eshell-test/flush-output): Update and simplify test given how eshell-match-result no longer moves point. --- test/lisp/eshell/eshell-tests-helpers.el | 9 +++++---- test/lisp/eshell/eshell-tests.el | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el index 2afa63ae51..a150adb144 100644 --- a/test/lisp/eshell/eshell-tests-helpers.el +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -66,10 +66,11 @@ eshell-insert-command (funcall (or func 'eshell-send-input))) (defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) + "Check that output of last command matches REGEXP." + (should + (string-match-p + regexp (buffer-substring-no-properties + (eshell-beginning-of-output) (eshell-end-of-output))))) (defun eshell-command-result-p (text regexp &optional func) "Insert a command at the end of the buffer." diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 6aeefdfde2..542815df80 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -232,9 +232,8 @@ eshell-test/flush-output (with-temp-eshell (eshell-insert-command "echo alpha") (eshell-kill-output) - (eshell-match-result (regexp-quote "*** output flushed ***\n")) - (should (forward-line)) - (should (= (point) eshell-last-output-start)))) + (eshell-match-result + (concat "^" (regexp-quote "*** output flushed ***\n") "$")))) (ert-deftest eshell-test/run-old-command () "Re-run an old command" -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #4: v5-0003-Add-Eshell-syntax-to-more-easily-bypass-Eshell-s-.patch --] [-- Type: text/x-patch, Size: 20799 bytes --] From abd8fd9b6ea7936c56cde538202c7f3eeb24aa76 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 17 Jan 2022 15:15:36 -0700 Subject: [PATCH v5 3/3] Add Eshell syntax to more easily bypass Eshell's own pipelining * etc/NEWS: * doc/misc/eshell.texi (Input/Output): Document the new syntax. * lisp/eshell/em-extpipe.el: New module (Bug#46351). * test/lisp/eshell/em-extpipe-tests.el: New tests. * lisp/eshell/esh-module.el (eshell-modules-list): Add `eshell-extpipe'. --- doc/misc/eshell.texi | 42 +++++ etc/NEWS | 10 ++ lisp/eshell/em-extpipe.el | 183 ++++++++++++++++++++ lisp/eshell/esh-module.el | 1 + test/lisp/eshell/em-extpipe-tests.el | 205 +++++++++++++++++++++++ test/lisp/eshell/eshell-tests-helpers.el | 4 +- 6 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 lisp/eshell/em-extpipe.el create mode 100644 test/lisp/eshell/em-extpipe-tests.el diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index df6e3b861e..261e88d00c 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1142,6 +1142,48 @@ Input/Output 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 +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 +executing commands on a remote machine using Eshell's Tramp +integration: using the remote shell's pipelining avoids copying the +data which will flow through the pipeline to local Emacs buffers and +then right back again. + +Eshell recognises a special syntax to make it easier to convert +pipelines so as to bypass Eshell's pipelining. Prefixing at least one +@code{|}, @code{<} or @code{>} with an asterisk marks a command as +intended for the operating system shell. To make it harder to invoke +this functionality accidentally, it is also required that the asterisk +be preceded by whitespace or located at the start of input. For +example, + +@example + cat *.ogg *| my-cool-decoder >file +@end example + +Executing this command will not copy all the data in the *.ogg files, +nor the decoded data, into Emacs buffers, as would normally happen. + +The command is interpreted as extending up to the next @code{|} +character which is not preceded by an unescaped asterisk following +whitespace, or the end of the input if there is no such character. +Thus, all @code{<} and @code{>} redirections occuring before the next +asterisk-unprefixed @code{|} are implicitly prefixed with (whitespace +and) asterisks. An exception is that Eshell-specific redirects right +at the end of the command are excluded. This allows input like this: + +@example + foo *| baz >#<buffer quux> +@end example + +@noindent which is equivalent to input like this: + +@example + sh -c "foo | baz" >#<buffer quux> +@end example + @node Extension modules @chapter Extension modules Eshell provides a facility for defining extension modules so that they diff --git a/etc/NEWS b/etc/NEWS index 5297db3e2d..68c0eba866 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -858,6 +858,16 @@ the Galeon web browser was released in September, 2008. *** New user option 'ruby-toggle-block-space-before-parameters'. +** Eshell + ++++ +*** New feature to easily bypass Eshell's own pipelining. +Prefixing '|', '<' or '>' with an asterisk, i.e. '*|', '*<' or '*>', +will cause the whole command to be passed to the operating system +shell. This is particularly useful to bypass Eshell's own pipelining +support for pipelines which will move a lot of data. See "Running +Shell Pipelines Natively" in the Eshell manual. + ** Miscellaneous --- diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el new file mode 100644 index 0000000000..57aeec38ff --- /dev/null +++ b/lisp/eshell/em-extpipe.el @@ -0,0 +1,183 @@ +;;; em-extpipe.el --- external shell pipelines -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + +;; 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 module tries to make +;; that easy to do. + +;;; Code: + +(require 'cl-lib) +(require 'esh-arg) +(require 'esh-io) +(require 'esh-util) + +(eval-when-compile (require 'files-x)) + +;;; Functions: + +(defun eshell-extpipe-initialize () ;Called from `eshell-mode' via intern-soft! + "Initialize external pipelines support." + (when (boundp 'eshell-special-chars-outside-quoting) + (setq-local + eshell-special-chars-outside-quoting + (append eshell-special-chars-outside-quoting (list ?\*)))) + (add-hook 'eshell-parse-argument-hook + #'eshell-parse-external-pipeline -20 t) + (add-hook 'eshell-pre-rewrite-command-hook + #'eshell-rewrite-external-pipeline -20 t)) + +(defun eshell-parse-external-pipeline () + "Parse a pipeline intended for execution by the external shell. + +A sequence of arguments is rewritten to use the operating system +shell when it contains `*|', `*<' or `*>', where the asterisk is +preceded by whitespace or located at the start of input. + +The command extends to the next `|' character which is not +preceded by an unescaped asterisk following whitespace, or the +end of input, except that any Eshell-specific output redirections +occurring at the end are excluded. Any other `<' or `>' +appearing before the end of the command are treated as though +preceded by (whitespace and) an asterisk. + +For example, + + foo <bar *| baz >#<buffer quux> + +is equivalent to + + sh -c \"foo <bar | baz\" >#<buffer quux> + +when `shell-file-name' is `sh' and `shell-command-switch' is +`-c', but in + + foo >#<buffer quux> *| baz + +and + + foo *| baz >#<buffer quux> --some-argument + +the Eshell-specific redirect will be passed on to the operating +system shell, probably leading to undesired results. + +This function must appear early in `eshell-parse-argument-hook' +to ensure that operating system shell syntax is not interpreted +as though it were Eshell syntax." + ;; Our goal is to wrap the external command to protect it from the + ;; other members of `eshell-parse-argument-hook'. We must avoid + ;; misinterpreting a quoted `*|', `*<' or `*>' as indicating an + ;; external pipeline, hence the structure of the loop in `findbeg1'. + (cl-flet + ((findbeg1 (pat &optional go (bound (point-max))) + (let* ((start (point)) + (result + (catch 'found + (while (> bound (point)) + (let* ((found + (save-excursion + (re-search-forward "['\"\\]" bound t))) + (next (or (and found (match-beginning 0)) + bound))) + (if (re-search-forward pat next t) + (throw 'found (match-beginning 1)) + (goto-char next) + (while (or (eshell-parse-backslash) + (eshell-parse-double-quote) + (eshell-parse-literal-quote))))))))) + (goto-char (if (and result go) (match-end 0) start)) + result))) + (unless (or eshell-current-argument eshell-current-quoted) + (let ((beg (point)) end + (next-marked (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)")) + (next-unmarked + (or (findbeg1 "\\(?:\\=\\|[^*]\\|\\S-\\*\\)\\(|\\)") + (point-max)))) + (when (and next-marked (> next-unmarked next-marked) + (or (> next-marked (point)) + (looking-back "\\`\\|\\s-" nil))) + ;; Skip to the final segment of the external pipeline. + (while (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*|\\)" t)) + ;; Find output redirections. + (while (findbeg1 + "\\([0-9]?>+&?[0-9]?\\s-*\\S-\\)" t next-unmarked) + ;; Is the output redirection Eshell-specific? We have our + ;; own logic, rather than calling `eshell-parse-argument', + ;; to avoid specifying here all the possible cars of + ;; parsed special references -- `get-buffer-create' etc. + (forward-char -1) + (let ((this-end + (save-match-data + (cond ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0)))))) + (cond ((and this-end end) + (goto-char this-end)) + (this-end + (goto-char this-end) + (setq end (match-beginning 0))) + (t + (setq end nil))))) + ;; We've moved past all Eshell-specific output redirections + ;; we could find. If there is only whitespace left, then + ;; `end' is right before redirections we should exclude; + ;; otherwise, we must include everything. + (unless (and end (skip-syntax-forward "\s" next-unmarked) + (= next-unmarked (point))) + (setq end next-unmarked)) + (let ((cmd (string-trim + (buffer-substring-no-properties beg end)))) + (goto-char end) + ;; We must now drop the asterisks, unless quoted/escaped. + (with-temp-buffer + (insert cmd) + (goto-char (point-min)) + (cl-loop + for next = (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)" t) + while next do (forward-char -2) (delete-char 1)) + (eshell-finish-arg + `(eshell-external-pipeline ,(buffer-string)))))))))) + +(defun eshell-rewrite-external-pipeline (terms) + "Rewrite an external pipeline in TERMS as parsed by +`eshell-parse-external-pipeline', which see." + (while terms + (when (and (listp (car terms)) + (eq (caar terms) 'eshell-external-pipeline)) + (with-connection-local-variables + (setcdr terms (cl-list* + shell-command-switch (cadar terms) (cdr terms))) + (setcar terms shell-file-name))) + (setq terms (cdr terms)))) + +(defsubst eshell-external-pipeline (&rest _args) + "Stub to generate an error if a pipeline is not rewritten." + (error "Unhandled external pipeline in input text")) + +(provide 'em-extpipe) +;;; esh-extpipe.el ends here diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el index ade151d7cd..14e91912d1 100644 --- a/lisp/eshell/esh-module.el +++ b/lisp/eshell/esh-module.el @@ -54,6 +54,7 @@ eshell-modules-list eshell-basic eshell-cmpl eshell-dirs + eshell-extpipe eshell-glob eshell-hist eshell-ls diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el new file mode 100644 index 0000000000..1283b6b361 --- /dev/null +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -0,0 +1,205 @@ +;;; em-extpipe-tests.el --- em-extpipe test suite -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + + +;;; Code: + +(require 'cl-lib) +(require 'ert) +(require 'ert-x) +(require 'em-extpipe) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) + +(defvar eshell-history-file-name) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-command-result-p "eshell-tests-helpers") + +(defmacro em-extpipe-tests--deftest (name input &rest body) + (declare (indent 2)) + `(ert-deftest ,name () + (cl-macrolet + ((should-parse (expected) + `(let ((shell-file-name "sh") + (shell-command-switch "-c")) + ;; Strip `eshell-trap-errors'. + (should (equal ,expected + (cadr (eshell-parse-command input)))))) + (with-substitute-for-temp (&rest body) + ;; Substitute name of an actual temporary file and/or + ;; buffer into `input'. The substitution logic is + ;; appropriate for only the use we put it to in this file. + `(ert-with-temp-file temp + (let ((temp-buffer (generate-new-buffer " *temp*" t))) + (unwind-protect + (let ((input + (replace-regexp-in-string + "temp\\([^>]\\|\\'\\)" temp + (string-replace "#<buffer temp>" + (buffer-name temp-buffer) + input)))) + ,@body) + (when (buffer-name temp-buffer) + (kill-buffer temp-buffer)))))) + (temp-should-string= (expected) + `(string= ,expected (string-trim-right + (with-temp-buffer + (insert-file-contents temp) + (buffer-string))))) + (temp-buffer-should-string= (expected) + `(string= ,expected (string-trim-right + (with-current-buffer temp-buffer + (buffer-string)))))) + (skip-unless shell-file-name) + (skip-unless shell-command-switch) + (skip-unless (executable-find shell-file-name)) + (let ((input ,input)) + (with-temp-eshell ,@body))))) + +(em-extpipe-tests--deftest em-extpipe-test-1 + "echo \"bar\" *| rev >temp" + (skip-unless (executable-find "rev")) + (should-parse '(eshell-named-command + "sh" (list "-c" "echo \"bar\" | rev >temp"))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-2 + "echo \"bar\" | rev *>temp" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "echo" (list (eshell-escape-arg "bar"))) + (eshell-named-command "sh" (list "-c" "rev >temp"))))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-3 "foo *| bar | baz -d" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "sh" (list "-c" "foo | bar")) + (eshell-named-command "baz" (list "-d")))))) + +(em-extpipe-tests--deftest em-extpipe-test-4 + "echo \"bar\" *| rev >#<buffer temp>" + (skip-unless (executable-find "rev")) + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "temp"))) + (eshell-named-command "sh" + (list "-c" "echo \"bar\" | rev")))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-buffer-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-5 + "foo *| bar >#<buffer quux> baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo | bar >#<buffer quux> baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-6 + "foo >#<buffer quux> *| bar baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo >#<buffer quux> | bar baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-7 + "foo *| bar >#<buffer quux> >>#<process other>" + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "quux"))) + (ignore + (eshell-set-output-handle 1 'append + (get-process "other"))) + (eshell-named-command "sh" + (list "-c" "foo | bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-8 + "foo *| bar >/dev/kill | baz" + (should-parse + '(eshell-execute-pipeline + '((progn + (ignore + (eshell-set-output-handle 1 'overwrite "/dev/kill")) + (eshell-named-command "sh" + (list "-c" "foo | bar"))) + (eshell-named-command "baz"))))) + +(em-extpipe-tests--deftest em-extpipe-test-9 "foo \\*| bar" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "foo" + (list (eshell-escape-arg "*"))) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-10 "foo \"*|\" *>bar" + (should-parse + '(eshell-named-command "sh" (list "-c" "foo \"*|\" >bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-11 "foo '*|' bar" + (should-parse '(eshell-named-command + "foo" (list (eshell-escape-arg "*|") "bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-12 ">foo bar *| baz" + (should-parse + '(eshell-named-command "sh" (list "-c" ">foo bar | baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-13 "foo*|bar" + (should-parse '(eshell-execute-pipeline + '((eshell-named-command (concat "foo" "*")) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-14 "tac *<temp" + (skip-unless (executable-find "tac")) + (should-parse '(eshell-named-command "sh" (list "-c" "tac <temp"))) + (with-substitute-for-temp + (with-temp-buffer (insert "bar\nbaz\n") (write-file temp)) + (eshell-command-result-p input "baz\nbar"))) + +(em-extpipe-tests--deftest em-extpipe-test-15 "echo \"bar\" *| cat" + (skip-unless (executable-find "cat")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | cat"))) + (cl-letf (((symbol-function 'eshell/cat) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "bar") + (eshell-command-result-p "echo \"bar\" | cat" "nonsense"))) + +(em-extpipe-tests--deftest em-extpipe-test-16 "echo \"bar\" *| rev" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | rev"))) + (let ((eshell-prefer-lisp-functions t)) + (cl-letf (((symbol-function 'rev) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "rab") + (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) + +;;; em-extpipe-tests.el ends here diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el index a150adb144..77f5313d57 100644 --- a/test/lisp/eshell/eshell-tests-helpers.el +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -86,6 +86,6 @@ eshell-test-command-result (let ((eshell-history-file-name nil)) (eshell-command-result command)))) -(provide 'eshell-tests) +(provide 'eshell-tests-helpers) -;;; eshell-tests.el ends here +;;; eshell-tests-helpers.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 20:32 ` Sean Whitton @ 2022-01-24 20:44 ` Sean Whitton 2022-01-24 20:48 ` Lars Ingebrigtsen 2022-01-24 20:46 ` Lars Ingebrigtsen 1 sibling, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-24 20:44 UTC (permalink / raw) To: Michael Albinus, Lars Ingebrigtsen; +Cc: 46351, Robert Pluim [-- Attachment #1: Type: text/plain, Size: 451 bytes --] Hello, On Mon 24 Jan 2022 at 01:32PM -07, Sean Whitton wrote: > On Mon 24 Jan 2022 at 03:18PM +01, Michael Albinus wrote: > >>> --- /dev/null >>> +++ b/test/lisp/eshell/eshell-tests-helpers.el >>> @@ -0,0 +1,90 @@ >>> +(provide 'eshell-tests) >>> + >>> +;;; eshell-tests.el ends here >> >> This shall be eshell-tests-helpers in both cases. > > Likewise fixed in the attached. ... but in the wrong patch, sigh. Revision attached. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v6-0001-Move-Eshell-test-helpers-to-their-own-file.patch --] [-- Type: text/x-patch, Size: 6969 bytes --] From 8b91bb7988f3ae905474bd80960a4e42f85108ca Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Fri, 21 Jan 2022 22:32:22 -0700 Subject: [PATCH v6 1/3] Move Eshell test helpers to their own file * test/lisp/eshell/eshell-tests.el: * test/lisp/eshell/eshell-tests-helpers.el: Move helpers to own file. --- test/lisp/eshell/eshell-tests-helpers.el | 90 ++++++++++++++++++++++++ test/lisp/eshell/eshell-tests.el | 61 +++------------- 2 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 test/lisp/eshell/eshell-tests-helpers.el diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el new file mode 100644 index 0000000000..0930f82284 --- /dev/null +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -0,0 +1,90 @@ +;;; eshell-tests-helpers.el --- Eshell test suite helpers -*- lexical-binding:t -*- + +;; Copyright (C) 1999-2022 Free Software Foundation, Inc. + +;; Author: John Wiegley <johnw@gnu.org> + +;; 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: + +;; Eshell test suite helpers. + +;;; Code: + +(require 'ert) +(require 'ert-x) +(require 'esh-mode) +(require 'eshell) + +(defvar eshell-test--max-subprocess-time 5 + "The maximum amount of time to wait for a subprocess to finish, in seconds. +See `eshell-wait-for-subprocess'.") + +(defmacro with-temp-eshell (&rest body) + "Evaluate BODY in a temporary Eshell buffer." + `(ert-with-temp-directory eshell-directory-name + (let* (;; We want no history file, so prevent Eshell from falling + ;; back on $HISTFILE. + (process-environment (cons "HISTFILE" process-environment)) + (eshell-history-file-name nil) + (eshell-buffer (eshell t))) + (unwind-protect + (with-current-buffer eshell-buffer + ,@body) + (let (kill-buffer-query-functions) + (kill-buffer eshell-buffer)))))) + +(defun eshell-wait-for-subprocess () + "Wait until there is no interactive subprocess running in Eshell. +If this takes longer than `eshell-test--max-subprocess-time', +raise an error." + (let ((start (current-time))) + (while (eshell-interactive-process) + (when (> (float-time (time-since start)) + eshell-test--max-subprocess-time) + (error "timed out waiting for subprocess")) + (sit-for 0.1)))) + +(defun eshell-insert-command (text &optional func) + "Insert a command at the end of the buffer." + (goto-char eshell-last-output-end) + (insert-and-inherit text) + (funcall (or func 'eshell-send-input))) + +(defun eshell-match-result (regexp) + "Check that text after `eshell-last-input-end' matches REGEXP." + (goto-char eshell-last-input-end) + (should (string-match-p regexp (buffer-substring-no-properties + (point) (point-max))))) + +(defun eshell-command-result-p (text regexp &optional func) + "Insert a command at the end of the buffer." + (eshell-insert-command text func) + (eshell-wait-for-subprocess) + (eshell-match-result regexp)) + +(defvar eshell-history-file-name) + +(defun eshell-test-command-result (command) + "Like `eshell-command-result', but not using HOME." + (ert-with-temp-directory eshell-directory-name + (let ((eshell-history-file-name nil)) + (eshell-command-result command)))) + +(provide 'eshell-tests-helpers) + +;;; eshell-tests-helpers.el ends here diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 1a7ab0ab06..6aeefdfde2 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -29,61 +29,16 @@ (require 'ert-x) (require 'esh-mode) (require 'eshell) - -(defvar eshell-test--max-subprocess-time 5 - "The maximum amount of time to wait for a subprocess to finish, in seconds. -See `eshell-wait-for-subprocess'.") - -(defmacro with-temp-eshell (&rest body) - "Evaluate BODY in a temporary Eshell buffer." - `(ert-with-temp-directory eshell-directory-name - (let* (;; We want no history file, so prevent Eshell from falling - ;; back on $HISTFILE. - (process-environment (cons "HISTFILE" process-environment)) - (eshell-history-file-name nil) - (eshell-buffer (eshell t))) - (unwind-protect - (with-current-buffer eshell-buffer - ,@body) - (let (kill-buffer-query-functions) - (kill-buffer eshell-buffer)))))) - -(defun eshell-wait-for-subprocess () - "Wait until there is no interactive subprocess running in Eshell. -If this takes longer than `eshell-test--max-subprocess-time', -raise an error." - (let ((start (current-time))) - (while (eshell-interactive-process) - (when (> (float-time (time-since start)) - eshell-test--max-subprocess-time) - (error "timed out waiting for subprocess")) - (sit-for 0.1)))) - -(defun eshell-insert-command (text &optional func) - "Insert a command at the end of the buffer." - (goto-char eshell-last-output-end) - (insert-and-inherit text) - (funcall (or func 'eshell-send-input))) - -(defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) - -(defun eshell-command-result-p (text regexp &optional func) - "Insert a command at the end of the buffer." - (eshell-insert-command text func) - (eshell-wait-for-subprocess) - (eshell-match-result regexp)) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) (defvar eshell-history-file-name) - -(defun eshell-test-command-result (command) - "Like `eshell-command-result', but not using HOME." - (ert-with-temp-directory eshell-directory-name - (let ((eshell-history-file-name nil)) - (eshell-command-result command)))) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-insert-command "eshell-tests-helpers") +(declare-function eshell-match-result "eshell-tests-helpers") +(declare-function eshell-command-result-p "eshell-tests-helpers") ;;; Tests: -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #3: v6-0002-Rework-eshell-match-result-for-testing-asynchrono.patch --] [-- Type: text/x-patch, Size: 2634 bytes --] From 40566a5d0c0ef4d30961915bac7875d078adb58e Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Sat, 22 Jan 2022 18:54:55 -0700 Subject: [PATCH v6 2/3] Rework eshell-match-result for testing asynchronous commands When using eshell-match-result via eshell-command-result-p to examine the output of asynchronous Eshell commands, a newly emitted prompt is included in the text against which the regexp is matched. This makes it awkward to match against the whole output; for example, to check whether it is empty. Rework the function to exclude the prompt. * test/lisp/eshell/eshell-tests-helpers.el (eshell-match-result): Exclude any newly emitted prompt from the text against which the regexp is matched. Additionally, the function no longer moves point. * test/lisp/eshell/eshell-tests.el (eshell-test/flush-output): Update and simplify test given how eshell-match-result no longer moves point. --- test/lisp/eshell/eshell-tests-helpers.el | 9 +++++---- test/lisp/eshell/eshell-tests.el | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el index 0930f82284..77f5313d57 100644 --- a/test/lisp/eshell/eshell-tests-helpers.el +++ b/test/lisp/eshell/eshell-tests-helpers.el @@ -66,10 +66,11 @@ eshell-insert-command (funcall (or func 'eshell-send-input))) (defun eshell-match-result (regexp) - "Check that text after `eshell-last-input-end' matches REGEXP." - (goto-char eshell-last-input-end) - (should (string-match-p regexp (buffer-substring-no-properties - (point) (point-max))))) + "Check that output of last command matches REGEXP." + (should + (string-match-p + regexp (buffer-substring-no-properties + (eshell-beginning-of-output) (eshell-end-of-output))))) (defun eshell-command-result-p (text regexp &optional func) "Insert a command at the end of the buffer." diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index 6aeefdfde2..542815df80 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -232,9 +232,8 @@ eshell-test/flush-output (with-temp-eshell (eshell-insert-command "echo alpha") (eshell-kill-output) - (eshell-match-result (regexp-quote "*** output flushed ***\n")) - (should (forward-line)) - (should (= (point) eshell-last-output-start)))) + (eshell-match-result + (concat "^" (regexp-quote "*** output flushed ***\n") "$")))) (ert-deftest eshell-test/run-old-command () "Re-run an old command" -- 2.30.2 [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #4: v6-0003-Add-Eshell-syntax-to-more-easily-bypass-Eshell-s-.patch --] [-- Type: text/x-patch, Size: 20262 bytes --] From aa30bf4b8fd3b9792149d95c86a15ecf573b99e6 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 17 Jan 2022 15:15:36 -0700 Subject: [PATCH v6 3/3] Add Eshell syntax to more easily bypass Eshell's own pipelining * etc/NEWS: * doc/misc/eshell.texi (Input/Output): Document the new syntax. * lisp/eshell/em-extpipe.el: New module (Bug#46351). * test/lisp/eshell/em-extpipe-tests.el: New tests. * lisp/eshell/esh-module.el (eshell-modules-list): Add `eshell-extpipe'. --- doc/misc/eshell.texi | 42 ++++++ etc/NEWS | 10 ++ lisp/eshell/em-extpipe.el | 183 +++++++++++++++++++++++ lisp/eshell/esh-module.el | 1 + test/lisp/eshell/em-extpipe-tests.el | 207 +++++++++++++++++++++++++++ 5 files changed, 443 insertions(+) create mode 100644 lisp/eshell/em-extpipe.el create mode 100644 test/lisp/eshell/em-extpipe-tests.el diff --git a/doc/misc/eshell.texi b/doc/misc/eshell.texi index df6e3b861e..261e88d00c 100644 --- a/doc/misc/eshell.texi +++ b/doc/misc/eshell.texi @@ -1142,6 +1142,48 @@ Input/Output 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 +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 +executing commands on a remote machine using Eshell's Tramp +integration: using the remote shell's pipelining avoids copying the +data which will flow through the pipeline to local Emacs buffers and +then right back again. + +Eshell recognises a special syntax to make it easier to convert +pipelines so as to bypass Eshell's pipelining. Prefixing at least one +@code{|}, @code{<} or @code{>} with an asterisk marks a command as +intended for the operating system shell. To make it harder to invoke +this functionality accidentally, it is also required that the asterisk +be preceded by whitespace or located at the start of input. For +example, + +@example + cat *.ogg *| my-cool-decoder >file +@end example + +Executing this command will not copy all the data in the *.ogg files, +nor the decoded data, into Emacs buffers, as would normally happen. + +The command is interpreted as extending up to the next @code{|} +character which is not preceded by an unescaped asterisk following +whitespace, or the end of the input if there is no such character. +Thus, all @code{<} and @code{>} redirections occuring before the next +asterisk-unprefixed @code{|} are implicitly prefixed with (whitespace +and) asterisks. An exception is that Eshell-specific redirects right +at the end of the command are excluded. This allows input like this: + +@example + foo *| baz >#<buffer quux> +@end example + +@noindent which is equivalent to input like this: + +@example + sh -c "foo | baz" >#<buffer quux> +@end example + @node Extension modules @chapter Extension modules Eshell provides a facility for defining extension modules so that they diff --git a/etc/NEWS b/etc/NEWS index 5297db3e2d..68c0eba866 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -858,6 +858,16 @@ the Galeon web browser was released in September, 2008. *** New user option 'ruby-toggle-block-space-before-parameters'. +** Eshell + ++++ +*** New feature to easily bypass Eshell's own pipelining. +Prefixing '|', '<' or '>' with an asterisk, i.e. '*|', '*<' or '*>', +will cause the whole command to be passed to the operating system +shell. This is particularly useful to bypass Eshell's own pipelining +support for pipelines which will move a lot of data. See "Running +Shell Pipelines Natively" in the Eshell manual. + ** Miscellaneous --- diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el new file mode 100644 index 0000000000..57aeec38ff --- /dev/null +++ b/lisp/eshell/em-extpipe.el @@ -0,0 +1,183 @@ +;;; em-extpipe.el --- external shell pipelines -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + +;; 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 module tries to make +;; that easy to do. + +;;; Code: + +(require 'cl-lib) +(require 'esh-arg) +(require 'esh-io) +(require 'esh-util) + +(eval-when-compile (require 'files-x)) + +;;; Functions: + +(defun eshell-extpipe-initialize () ;Called from `eshell-mode' via intern-soft! + "Initialize external pipelines support." + (when (boundp 'eshell-special-chars-outside-quoting) + (setq-local + eshell-special-chars-outside-quoting + (append eshell-special-chars-outside-quoting (list ?\*)))) + (add-hook 'eshell-parse-argument-hook + #'eshell-parse-external-pipeline -20 t) + (add-hook 'eshell-pre-rewrite-command-hook + #'eshell-rewrite-external-pipeline -20 t)) + +(defun eshell-parse-external-pipeline () + "Parse a pipeline intended for execution by the external shell. + +A sequence of arguments is rewritten to use the operating system +shell when it contains `*|', `*<' or `*>', where the asterisk is +preceded by whitespace or located at the start of input. + +The command extends to the next `|' character which is not +preceded by an unescaped asterisk following whitespace, or the +end of input, except that any Eshell-specific output redirections +occurring at the end are excluded. Any other `<' or `>' +appearing before the end of the command are treated as though +preceded by (whitespace and) an asterisk. + +For example, + + foo <bar *| baz >#<buffer quux> + +is equivalent to + + sh -c \"foo <bar | baz\" >#<buffer quux> + +when `shell-file-name' is `sh' and `shell-command-switch' is +`-c', but in + + foo >#<buffer quux> *| baz + +and + + foo *| baz >#<buffer quux> --some-argument + +the Eshell-specific redirect will be passed on to the operating +system shell, probably leading to undesired results. + +This function must appear early in `eshell-parse-argument-hook' +to ensure that operating system shell syntax is not interpreted +as though it were Eshell syntax." + ;; Our goal is to wrap the external command to protect it from the + ;; other members of `eshell-parse-argument-hook'. We must avoid + ;; misinterpreting a quoted `*|', `*<' or `*>' as indicating an + ;; external pipeline, hence the structure of the loop in `findbeg1'. + (cl-flet + ((findbeg1 (pat &optional go (bound (point-max))) + (let* ((start (point)) + (result + (catch 'found + (while (> bound (point)) + (let* ((found + (save-excursion + (re-search-forward "['\"\\]" bound t))) + (next (or (and found (match-beginning 0)) + bound))) + (if (re-search-forward pat next t) + (throw 'found (match-beginning 1)) + (goto-char next) + (while (or (eshell-parse-backslash) + (eshell-parse-double-quote) + (eshell-parse-literal-quote))))))))) + (goto-char (if (and result go) (match-end 0) start)) + result))) + (unless (or eshell-current-argument eshell-current-quoted) + (let ((beg (point)) end + (next-marked (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)")) + (next-unmarked + (or (findbeg1 "\\(?:\\=\\|[^*]\\|\\S-\\*\\)\\(|\\)") + (point-max)))) + (when (and next-marked (> next-unmarked next-marked) + (or (> next-marked (point)) + (looking-back "\\`\\|\\s-" nil))) + ;; Skip to the final segment of the external pipeline. + (while (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*|\\)" t)) + ;; Find output redirections. + (while (findbeg1 + "\\([0-9]?>+&?[0-9]?\\s-*\\S-\\)" t next-unmarked) + ;; Is the output redirection Eshell-specific? We have our + ;; own logic, rather than calling `eshell-parse-argument', + ;; to avoid specifying here all the possible cars of + ;; parsed special references -- `get-buffer-create' etc. + (forward-char -1) + (let ((this-end + (save-match-data + (cond ((looking-at "#<") + (forward-char 1) + (1+ (eshell-find-delimiter ?\< ?\>))) + ((and (looking-at "/\\S-+") + (assoc (match-string 0) + eshell-virtual-targets)) + (match-end 0)))))) + (cond ((and this-end end) + (goto-char this-end)) + (this-end + (goto-char this-end) + (setq end (match-beginning 0))) + (t + (setq end nil))))) + ;; We've moved past all Eshell-specific output redirections + ;; we could find. If there is only whitespace left, then + ;; `end' is right before redirections we should exclude; + ;; otherwise, we must include everything. + (unless (and end (skip-syntax-forward "\s" next-unmarked) + (= next-unmarked (point))) + (setq end next-unmarked)) + (let ((cmd (string-trim + (buffer-substring-no-properties beg end)))) + (goto-char end) + ;; We must now drop the asterisks, unless quoted/escaped. + (with-temp-buffer + (insert cmd) + (goto-char (point-min)) + (cl-loop + for next = (findbeg1 "\\(?:\\=\\|\\s-\\)\\(\\*[|<>]\\)" t) + while next do (forward-char -2) (delete-char 1)) + (eshell-finish-arg + `(eshell-external-pipeline ,(buffer-string)))))))))) + +(defun eshell-rewrite-external-pipeline (terms) + "Rewrite an external pipeline in TERMS as parsed by +`eshell-parse-external-pipeline', which see." + (while terms + (when (and (listp (car terms)) + (eq (caar terms) 'eshell-external-pipeline)) + (with-connection-local-variables + (setcdr terms (cl-list* + shell-command-switch (cadar terms) (cdr terms))) + (setcar terms shell-file-name))) + (setq terms (cdr terms)))) + +(defsubst eshell-external-pipeline (&rest _args) + "Stub to generate an error if a pipeline is not rewritten." + (error "Unhandled external pipeline in input text")) + +(provide 'em-extpipe) +;;; esh-extpipe.el ends here diff --git a/lisp/eshell/esh-module.el b/lisp/eshell/esh-module.el index ade151d7cd..14e91912d1 100644 --- a/lisp/eshell/esh-module.el +++ b/lisp/eshell/esh-module.el @@ -54,6 +54,7 @@ eshell-modules-list eshell-basic eshell-cmpl eshell-dirs + eshell-extpipe eshell-glob eshell-hist eshell-ls diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el new file mode 100644 index 0000000000..8b69e0504d --- /dev/null +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -0,0 +1,207 @@ +;;; em-extpipe-tests.el --- em-extpipe test suite -*- lexical-binding:t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author: Sean Whitton <spwhitton@spwhitton.name> + +;; 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: + + +;;; Code: + +(require 'cl-lib) +(require 'ert) +(require 'ert-x) +(require 'em-extpipe) +(eval-and-compile + (load (expand-file-name "eshell-tests-helpers" + (file-name-directory (or load-file-name + default-directory))))) + +(defvar eshell-history-file-name) +(defvar eshell-test--max-subprocess-time) +(declare-function eshell-command-result-p "eshell-tests-helpers") + +(defmacro em-extpipe-tests--deftest (name input &rest body) + (declare (indent 2)) + `(ert-deftest ,name () + (cl-macrolet + ((should-parse (expected) + `(let ((shell-file-name "sh") + (shell-command-switch "-c")) + ;; Strip `eshell-trap-errors'. + (should (equal ,expected + (cadr (eshell-parse-command input)))))) + (with-substitute-for-temp (&rest body) + ;; Substitute name of an actual temporary file and/or + ;; buffer into `input'. The substitution logic is + ;; appropriate for only the use we put it to in this file. + `(ert-with-temp-file temp + (let ((temp-buffer (generate-new-buffer " *temp*" t))) + (unwind-protect + (let ((input + (replace-regexp-in-string + "temp\\([^>]\\|\\'\\)" temp + (string-replace "#<buffer temp>" + (buffer-name temp-buffer) + input)))) + ,@body) + (when (buffer-name temp-buffer) + (kill-buffer temp-buffer)))))) + (temp-should-string= (expected) + `(string= ,expected (string-trim-right + (with-temp-buffer + (insert-file-contents temp) + (buffer-string))))) + (temp-buffer-should-string= (expected) + `(string= ,expected (string-trim-right + (with-current-buffer temp-buffer + (buffer-string)))))) + (skip-unless shell-file-name) + (skip-unless shell-command-switch) + (skip-unless (executable-find shell-file-name)) + (let ((input ,input)) + (with-temp-eshell ,@body))))) + +(em-extpipe-tests--deftest em-extpipe-test-1 + "echo \"bar\" *| rev >temp" + (skip-unless (executable-find "rev")) + (should-parse '(eshell-named-command + "sh" (list "-c" "echo \"bar\" | rev >temp"))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-2 + "echo \"bar\" | rev *>temp" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "echo" (list (eshell-escape-arg "bar"))) + (eshell-named-command "sh" (list "-c" "rev >temp"))))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-3 "foo *| bar | baz -d" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "sh" (list "-c" "foo | bar")) + (eshell-named-command "baz" (list "-d")))))) + +(em-extpipe-tests--deftest em-extpipe-test-4 + "echo \"bar\" *| rev >#<buffer temp>" + (skip-unless (executable-find "rev")) + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "temp"))) + (eshell-named-command "sh" + (list "-c" "echo \"bar\" | rev")))) + (with-substitute-for-temp + (eshell-command-result-p input "^$") + (temp-buffer-should-string= "rab"))) + +(em-extpipe-tests--deftest em-extpipe-test-5 + "foo *| bar >#<buffer quux> baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo | bar >#<buffer quux> baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-6 + "foo >#<buffer quux> *| bar baz" + (should-parse '(eshell-named-command + "sh" (list "-c" "foo >#<buffer quux> | bar baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-7 + "foo *| bar >#<buffer quux> >>#<process other>" + (should-parse + '(progn + (ignore + (eshell-set-output-handle 1 'overwrite + (get-buffer-create "quux"))) + (ignore + (eshell-set-output-handle 1 'append + (get-process "other"))) + (eshell-named-command "sh" + (list "-c" "foo | bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-8 + "foo *| bar >/dev/kill | baz" + (should-parse + '(eshell-execute-pipeline + '((progn + (ignore + (eshell-set-output-handle 1 'overwrite "/dev/kill")) + (eshell-named-command "sh" + (list "-c" "foo | bar"))) + (eshell-named-command "baz"))))) + +(em-extpipe-tests--deftest em-extpipe-test-9 "foo \\*| bar" + (should-parse + '(eshell-execute-pipeline + '((eshell-named-command "foo" + (list (eshell-escape-arg "*"))) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-10 "foo \"*|\" *>bar" + (should-parse + '(eshell-named-command "sh" (list "-c" "foo \"*|\" >bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-11 "foo '*|' bar" + (should-parse '(eshell-named-command + "foo" (list (eshell-escape-arg "*|") "bar")))) + +(em-extpipe-tests--deftest em-extpipe-test-12 ">foo bar *| baz" + (should-parse + '(eshell-named-command "sh" (list "-c" ">foo bar | baz")))) + +(em-extpipe-tests--deftest em-extpipe-test-13 "foo*|bar" + (should-parse '(eshell-execute-pipeline + '((eshell-named-command (concat "foo" "*")) + (eshell-named-command "bar"))))) + +(em-extpipe-tests--deftest em-extpipe-test-14 "tac *<temp" + (skip-unless (executable-find "tac")) + (should-parse '(eshell-named-command "sh" (list "-c" "tac <temp"))) + (with-substitute-for-temp + (with-temp-buffer (insert "bar\nbaz\n") (write-file temp)) + (eshell-command-result-p input "baz\nbar"))) + +(em-extpipe-tests--deftest em-extpipe-test-15 "echo \"bar\" *| cat" + (skip-unless (executable-find "cat")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | cat"))) + (cl-letf (((symbol-function 'eshell/cat) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "bar") + (eshell-command-result-p "echo \"bar\" | cat" "nonsense"))) + +(em-extpipe-tests--deftest em-extpipe-test-16 "echo \"bar\" *| rev" + (skip-unless (executable-find "rev")) + (should-parse + '(eshell-named-command "sh" (list "-c" "echo \"bar\" | rev"))) + (let ((eshell-prefer-lisp-functions t)) + (cl-letf (((symbol-function 'rev) + (lambda (&rest _args) (eshell-print "nonsense")))) + (eshell-command-result-p input "rab") + (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) + +(provide 'em-extpipe-tests) + +;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 20:44 ` Sean Whitton @ 2022-01-24 20:48 ` Lars Ingebrigtsen 2022-01-24 21:42 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-24 20:48 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Robert Pluim, Michael Albinus Sean Whitton <spwhitton@spwhitton.name> writes: > ... but in the wrong patch, sigh. Revision attached. Oops; the old one is already pushed. Can you supply an additional patch for this? -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 20:48 ` Lars Ingebrigtsen @ 2022-01-24 21:42 ` Sean Whitton 2022-01-24 21:51 ` Lars Ingebrigtsen 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-24 21:42 UTC (permalink / raw) To: Lars Ingebrigtsen; +Cc: 46351, Robert Pluim, Michael Albinus [-- Attachment #1: Type: text/plain, Size: 551 bytes --] Hello, On Mon 24 Jan 2022 at 09:48PM +01, Lars Ingebrigtsen wrote: > Sean Whitton <spwhitton@spwhitton.name> writes: > >> ... but in the wrong patch, sigh. Revision attached. > > Oops; the old one is already pushed. Can you supply an additional patch > for this? I put the "... ends here" fix into 667e212048 rather than 1693423fd7 which is not now fixable. However, I also noticed that em-extpipe-tests.el lacks a `provide' form so here is a fix for that. Pretty sure that's it for now. Thank you for reviewing and applying! -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: 0001-Fix-missing-provide-in-recently-introduced-em-extpip.patch --] [-- Type: text/x-patch, Size: 767 bytes --] From 67017d9f13ce6f4c2ae6106b84db8d883b8ed551 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 24 Jan 2022 14:40:51 -0700 Subject: [PATCH] ; Fix missing `provide' in recently introduced em-extpipe-tests.el --- test/lisp/eshell/em-extpipe-tests.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el index 1283b6b361..8b69e0504d 100644 --- a/test/lisp/eshell/em-extpipe-tests.el +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -202,4 +202,6 @@ em-extpipe-test-16 (eshell-command-result-p input "rab") (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) +(provide 'em-extpipe-tests) + ;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 21:42 ` Sean Whitton @ 2022-01-24 21:51 ` Lars Ingebrigtsen 2022-01-24 22:48 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-24 21:51 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Robert Pluim, Michael Albinus Sean Whitton <spwhitton@spwhitton.name> writes: > I put the "... ends here" fix into 667e212048 rather than 1693423fd7 > which is not now fixable. However, I also noticed that > em-extpipe-tests.el lacks a `provide' form so here is a fix for that. > Pretty sure that's it for now. Thank you for reviewing and applying! We don't usually add provides in the test files, so that doesn't seem necessary. -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 21:51 ` Lars Ingebrigtsen @ 2022-01-24 22:48 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-24 22:48 UTC (permalink / raw) To: Lars Ingebrigtsen; +Cc: 46351, Robert Pluim, Michael Albinus Hello, On Mon 24 Jan 2022 at 10:51PM +01, Lars Ingebrigtsen wrote: > Sean Whitton <spwhitton@spwhitton.name> writes: > >> I put the "... ends here" fix into 667e212048 rather than 1693423fd7 >> which is not now fixable. However, I also noticed that >> em-extpipe-tests.el lacks a `provide' form so here is a fix for that. >> Pretty sure that's it for now. Thank you for reviewing and applying! > > We don't usually add provides in the test files, so that doesn't seem > necessary. That makes more sense to me, I just saw it in eshell-tests.el today. Thanks again. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 20:32 ` Sean Whitton 2022-01-24 20:44 ` Sean Whitton @ 2022-01-24 20:46 ` Lars Ingebrigtsen 2022-01-25 2:39 ` Jim Porter 1 sibling, 1 reply; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-24 20:46 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351, Robert Pluim, Michael Albinus Sean Whitton <spwhitton@spwhitton.name> writes: > Likewise fixed in the attached. Thanks; pushed to Emacs 29. -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-24 20:46 ` Lars Ingebrigtsen @ 2022-01-25 2:39 ` Jim Porter 2022-01-25 5:33 ` bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols Sean Whitton ` (2 more replies) 0 siblings, 3 replies; 60+ messages in thread From: Jim Porter @ 2022-01-25 2:39 UTC (permalink / raw) To: Lars Ingebrigtsen, Sean Whitton; +Cc: 46351, Robert Pluim, Michael Albinus On 1/24/2022 12:46 PM, Lars Ingebrigtsen wrote: > Sean Whitton <spwhitton@spwhitton.name> writes: > >> Likewise fixed in the attached. > > Thanks; pushed to Emacs 29. I just noticed a small bit of breakage with this. It's no longer possible to refer to Lisp functions in Eshell like so: #'upcase Eshell explicitly supports this construct (see `eshell-lisp-regexp'), though it doesn't appear to be documented in the manual. Currently, this syntax is only occasionally useful, but I'm working on a patch series where it'll likely become a lot more common. My patches will add support for piping to Lisp functions, so that you can do the following, for example: $ echo hi | #'upcase HI It looks like the breakage in parsing #'upcase is the result of `eshell-parse-external-pipeline' trying to skip over args like '*|' (with the quotes). However, it sees the single-quote in #'upcase and then calls `eshell-parse-literal-quote', resulting in the error message: Expecting completion of delimiter ' ... ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 2:39 ` Jim Porter @ 2022-01-25 5:33 ` Sean Whitton 2022-01-25 8:50 ` Sean Whitton 2022-01-25 8:54 ` bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Michael Albinus 2 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-25 5:33 UTC (permalink / raw) To: 53518; +Cc: Jim Porter Hello Jim, On Mon 24 Jan 2022 at 06:39pm -08, Jim Porter wrote: > I just noticed a small bit of breakage with this. It's no longer > possible to refer to Lisp functions in Eshell like so: > > #'upcase > > Eshell explicitly supports this construct (see `eshell-lisp-regexp'), > though it doesn't appear to be documented in the manual. Currently, this > syntax is only occasionally useful, but I'm working on a patch series > where it'll likely become a lot more common. My patches will add support > for piping to Lisp functions, so that you can do the following, for example: > > $ echo hi | #'upcase > HI > > It looks like the breakage in parsing #'upcase is the result of > `eshell-parse-external-pipeline' trying to skip over args like '*|' > (with the quotes). However, it sees the single-quote in #'upcase and > then calls `eshell-parse-literal-quote', resulting in the error message: > > Expecting completion of delimiter ' ... Thanks for the report. I have a fix; incoming once I have a bug number. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 2:39 ` Jim Porter 2022-01-25 5:33 ` bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols Sean Whitton @ 2022-01-25 8:50 ` Sean Whitton 2022-01-25 12:26 ` Lars Ingebrigtsen 2022-01-25 18:14 ` Jim Porter 2022-01-25 8:54 ` bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Michael Albinus 2 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-25 8:50 UTC (permalink / raw) To: 53518, Jim Porter; +Cc: Lars Ingebrigtsen, Michael Albinus [-- Attachment #1: Type: text/plain, Size: 1261 bytes --] Hello again, On Mon 24 Jan 2022 at 06:39pm -08, Jim Porter wrote: > I just noticed a small bit of breakage with this. It's no longer > possible to refer to Lisp functions in Eshell like so: > > #'upcase > > Eshell explicitly supports this construct (see `eshell-lisp-regexp'), > though it doesn't appear to be documented in the manual. Currently, this > syntax is only occasionally useful, but I'm working on a patch series > where it'll likely become a lot more common. My patches will add support > for piping to Lisp functions, so that you can do the following, for example: > > $ echo hi | #'upcase > HI Out of curiosity, why is there a need for the sharpquote? Why not just 'echo hi | upcase'? Is it to do with requesting the new piping? > It looks like the breakage in parsing #'upcase is the result of > `eshell-parse-external-pipeline' trying to skip over args like '*|' > (with the quotes). However, it sees the single-quote in #'upcase and > then calls `eshell-parse-literal-quote', resulting in the error message: > > Expecting completion of delimiter ' ... Here is a patch which has `eshell-parse-external-pipeline' try skipping over sharp-quoted symbols before trying to skip over single-quoted strings. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: 0001-Fix-input-of-sharp-quoted-symbols-in-Eshell-with-em-.patch --] [-- Type: text/x-diff, Size: 2883 bytes --] From 8725495cb91ebf831cd623c58f84a4347e6f1651 Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 24 Jan 2022 22:39:15 -0700 Subject: [PATCH] Fix input of sharp-quoted symbols in Eshell with em-extpipe * lisp/eshell/em-extpipe.el (eshell-parse-external-pipeline): Fix misinterpreting sharp-quoted symbols as the beginning of single-quoted strings (Bug#53518). Add protection against a possible infinite loop. * test/lisp/eshell/em-extpipe-tests.el (em-extpipe-test-17): New test. --- lisp/eshell/em-extpipe.el | 12 +++++++++--- test/lisp/eshell/em-extpipe-tests.el | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el index 57aeec38ff..b0f92e41dc 100644 --- a/lisp/eshell/em-extpipe.el +++ b/lisp/eshell/em-extpipe.el @@ -97,15 +97,21 @@ eshell-parse-external-pipeline (while (> bound (point)) (let* ((found (save-excursion - (re-search-forward "['\"\\]" bound t))) + (re-search-forward + "\\(?:#?'\\|\"\\|\\\\\\)" bound t))) (next (or (and found (match-beginning 0)) bound))) (if (re-search-forward pat next t) (throw 'found (match-beginning 1)) (goto-char next) - (while (or (eshell-parse-backslash) + (while (or (eshell-parse-lisp-argument) + (eshell-parse-backslash) (eshell-parse-double-quote) - (eshell-parse-literal-quote))))))))) + (eshell-parse-literal-quote))) + ;; Guard against an infinite loop if none of + ;; the parsers moved us forward. + (unless (or (> (point) next) (eobp)) + (forward-char 1)))))))) (goto-char (if (and result go) (match-end 0) start)) result))) (unless (or eshell-current-argument eshell-current-quoted) diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el index 1283b6b361..0879ad5b0c 100644 --- a/test/lisp/eshell/em-extpipe-tests.el +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -202,4 +202,8 @@ em-extpipe-test-16 (eshell-command-result-p input "rab") (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) +;; Confirm we don't break input of sharp-quoted symbols (Bug#53518). +(em-extpipe-tests--deftest em-extpipe-test-17 "funcall #'upcase foo" + (eshell-command-result-p input "FOO")) + ;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 8:50 ` Sean Whitton @ 2022-01-25 12:26 ` Lars Ingebrigtsen 2022-01-25 16:48 ` Sean Whitton 2022-01-25 18:14 ` Jim Porter 1 sibling, 1 reply; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-25 12:26 UTC (permalink / raw) To: Sean Whitton; +Cc: 53518, Michael Albinus, Jim Porter Sean Whitton <spwhitton@spwhitton.name> writes: > Here is a patch which has `eshell-parse-external-pipeline' try skipping > over sharp-quoted symbols before trying to skip over single-quoted > strings. This leads to the following compilation warning: ELC eshell/em-extpipe.elc In end of data: eshell/em-extpipe.el:107:38: Warning: the function `eshell-parse-lisp-argument' is not known to be defined. -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 12:26 ` Lars Ingebrigtsen @ 2022-01-25 16:48 ` Sean Whitton 2022-01-26 5:38 ` Jim Porter 2022-01-26 13:13 ` Lars Ingebrigtsen 0 siblings, 2 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-25 16:48 UTC (permalink / raw) To: Lars Ingebrigtsen; +Cc: 53518, Michael Albinus, Jim Porter [-- Attachment #1: Type: text/plain, Size: 348 bytes --] Hello, On Tue 25 Jan 2022 at 01:26pm +01, Lars Ingebrigtsen wrote: > This leads to the following compilation warning: > > ELC eshell/em-extpipe.elc > In end of data: > eshell/em-extpipe.el:107:38: Warning: the function `eshell-parse-lisp-argument' is not known to be defined. Apologies for this. Here is a fixed patch. -- Sean Whitton [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: v2-0001-Fix-input-of-sharp-quoted-symbols-in-Eshell-with-.patch --] [-- Type: text/x-diff, Size: 3009 bytes --] From 7981a5de5f06fd8879e57ac4d780370965999d7b Mon Sep 17 00:00:00 2001 From: Sean Whitton <spwhitton@spwhitton.name> Date: Mon, 24 Jan 2022 22:39:15 -0700 Subject: [PATCH v2] Fix input of sharp-quoted symbols in Eshell with em-extpipe * lisp/eshell/em-extpipe.el (eshell-parse-external-pipeline): Fix misinterpreting sharp-quoted symbols as the beginning of single-quoted strings (Bug#53518). Add protection against a possible infinite loop. * test/lisp/eshell/em-extpipe-tests.el (em-extpipe-test-17): New test. --- lisp/eshell/em-extpipe.el | 13 ++++++++++--- test/lisp/eshell/em-extpipe-tests.el | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lisp/eshell/em-extpipe.el b/lisp/eshell/em-extpipe.el index 57aeec38ff..eb5b3bfe1d 100644 --- a/lisp/eshell/em-extpipe.el +++ b/lisp/eshell/em-extpipe.el @@ -30,6 +30,7 @@ (require 'cl-lib) (require 'esh-arg) +(require 'esh-cmd) (require 'esh-io) (require 'esh-util) @@ -97,15 +98,21 @@ eshell-parse-external-pipeline (while (> bound (point)) (let* ((found (save-excursion - (re-search-forward "['\"\\]" bound t))) + (re-search-forward + "\\(?:#?'\\|\"\\|\\\\\\)" bound t))) (next (or (and found (match-beginning 0)) bound))) (if (re-search-forward pat next t) (throw 'found (match-beginning 1)) (goto-char next) - (while (or (eshell-parse-backslash) + (while (or (eshell-parse-lisp-argument) + (eshell-parse-backslash) (eshell-parse-double-quote) - (eshell-parse-literal-quote))))))))) + (eshell-parse-literal-quote))) + ;; Guard against an infinite loop if none of + ;; the parsers moved us forward. + (unless (or (> (point) next) (eobp)) + (forward-char 1)))))))) (goto-char (if (and result go) (match-end 0) start)) result))) (unless (or eshell-current-argument eshell-current-quoted) diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el index 1283b6b361..0879ad5b0c 100644 --- a/test/lisp/eshell/em-extpipe-tests.el +++ b/test/lisp/eshell/em-extpipe-tests.el @@ -202,4 +202,8 @@ em-extpipe-test-16 (eshell-command-result-p input "rab") (eshell-command-result-p "echo \"bar\" | rev" "nonsense")))) +;; Confirm we don't break input of sharp-quoted symbols (Bug#53518). +(em-extpipe-tests--deftest em-extpipe-test-17 "funcall #'upcase foo" + (eshell-command-result-p input "FOO")) + ;;; em-extpipe-tests.el ends here -- 2.30.2 ^ permalink raw reply related [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 16:48 ` Sean Whitton @ 2022-01-26 5:38 ` Jim Porter 2022-01-26 13:13 ` Lars Ingebrigtsen 1 sibling, 0 replies; 60+ messages in thread From: Jim Porter @ 2022-01-26 5:38 UTC (permalink / raw) To: Sean Whitton, Lars Ingebrigtsen; +Cc: 53518, Michael Albinus On 1/25/2022 8:48 AM, Sean Whitton wrote: > On Tue 25 Jan 2022 at 01:26pm +01, Lars Ingebrigtsen wrote: > >> This leads to the following compilation warning: >> >> ELC eshell/em-extpipe.elc >> In end of data: >> eshell/em-extpipe.el:107:38: Warning: the function `eshell-parse-lisp-argument' is not known to be defined. > > Apologies for this. Here is a fixed patch. Thanks for the quick fix, this resolves everything on my end. I applied the patch to my local branch for Lisp function pipelining and all the tests in test/lisp/eshell/ now pass, including the new ones I'm working on. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 16:48 ` Sean Whitton 2022-01-26 5:38 ` Jim Porter @ 2022-01-26 13:13 ` Lars Ingebrigtsen 1 sibling, 0 replies; 60+ messages in thread From: Lars Ingebrigtsen @ 2022-01-26 13:13 UTC (permalink / raw) To: Sean Whitton; +Cc: 53518, Michael Albinus, Jim Porter Sean Whitton <spwhitton@spwhitton.name> writes: > Apologies for this. Here is a fixed patch. Thanks; applied to Emacs 29. -- (domestic pets only, the antidote for overdose, milk.) bloggy blog: http://lars.ingebrigtsen.no ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 8:50 ` Sean Whitton 2022-01-25 12:26 ` Lars Ingebrigtsen @ 2022-01-25 18:14 ` Jim Porter 2022-01-25 20:01 ` Sean Whitton 1 sibling, 1 reply; 60+ messages in thread From: Jim Porter @ 2022-01-25 18:14 UTC (permalink / raw) To: Sean Whitton, 53518; +Cc: Lars Ingebrigtsen, Michael Albinus On 1/25/2022 12:50 AM, Sean Whitton wrote: > Hello again, > > On Mon 24 Jan 2022 at 06:39pm -08, Jim Porter wrote: > >> I just noticed a small bit of breakage with this. It's no longer >> possible to refer to Lisp functions in Eshell like so: >> >> #'upcase >> >> Eshell explicitly supports this construct (see `eshell-lisp-regexp'), >> though it doesn't appear to be documented in the manual. Currently, this >> syntax is only occasionally useful, but I'm working on a patch series >> where it'll likely become a lot more common. My patches will add support >> for piping to Lisp functions, so that you can do the following, for example: >> >> $ echo hi | #'upcase >> HI > > Out of curiosity, why is there a need for the sharpquote? Why not just > 'echo hi | upcase'? Is it to do with requesting the new piping? It becomes more relevant with my WIP patches to support piping to Lisp functions, but it means something different today too. "upcase" *calls* the function `upcase', whereas "#'upcase" evaluates to the function object itself. For example: $ upcase Wrong number of arguments: #<subr upcase>, 0 $ #'upcase upcase Or, for a slightly different, but more practical example today: $ mapcar upcase $(list "foo" "bar") Invalid function: "upcase" $ mapcar #'upcase $(list "foo" "bar") ("FOO" "BAR") In this case, "upcase" in the first command is just the string "upcase", whereas "#'upcase" refers to the function as in the other case. The shortest other way I'm aware of to spell that using shell-like invocation would be: $ mapcar $(quote upcase) $(list "foo" "bar") ("FOO" "BAR") For testing purposes, it would probably also be useful to ensure that Lisp syntax works too: $ (mapcar #'upcase '("foo" "bar")) ("FOO" "BAR") Just for the sake of completeness, in my WIP patches, "echo hi | #'upcase" and "echo hi | upcase" will also do different things, following the above precedent. The former pipes the output of echo to the function upcase. The latter pipes the output of echo to the *result* of calling the function upcase with no arguments. In addition to being consistent with how Eshell currently works, this allows you to do things like "echo hi | less -N", where "less -N" is evaluated as an Eshell command and then returns a pseudo-pipe for echo to connect to. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 18:14 ` Jim Porter @ 2022-01-25 20:01 ` Sean Whitton 2022-01-25 20:52 ` Jim Porter 0 siblings, 1 reply; 60+ messages in thread From: Sean Whitton @ 2022-01-25 20:01 UTC (permalink / raw) To: Jim Porter, 53518; +Cc: Michael Albinus Hello, On Tue 25 Jan 2022 at 10:14AM -08, Jim Porter wrote: > It becomes more relevant with my WIP patches to support piping to Lisp > functions, but it means something different today too. Indeed. > Just for the sake of completeness, in my WIP patches, "echo hi | > #'upcase" and "echo hi | upcase" will also do different things, > following the above precedent. The former pipes the output of echo to > the function upcase. The latter pipes the output of echo to the *result* > of calling the function upcase with no arguments. Okay, thanks. I asked because I thought you were only planning support the former case. > In addition to being consistent with how Eshell currently works, this > allows you to do things like "echo hi | less -N", where "less -N" is > evaluated as an Eshell command and then returns a pseudo-pipe for echo > to connect to. It's not quite clear to me how this is consistent with how Eshell currently works. When you type "echo hi | cat" is it right to say that "cat" is evaluated to produce the thing to which the output of the first command is piped? Perhaps I'm not thinking broadly enough. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 20:01 ` Sean Whitton @ 2022-01-25 20:52 ` Jim Porter 2022-01-25 22:38 ` Sean Whitton 0 siblings, 1 reply; 60+ messages in thread From: Jim Porter @ 2022-01-25 20:52 UTC (permalink / raw) To: Sean Whitton, 53518; +Cc: Michael Albinus On 1/25/2022 12:01 PM, Sean Whitton wrote: >> In addition to being consistent with how Eshell currently works, this >> allows you to do things like "echo hi | less -N", where "less -N" is >> evaluated as an Eshell command and then returns a pseudo-pipe for echo >> to connect to. > > It's not quite clear to me how this is consistent with how Eshell > currently works. When you type "echo hi | cat" is it right to say that > "cat" is evaluated to produce the thing to which the output of the first > command is piped? Perhaps I'm not thinking broadly enough. In general, I just mean that when running a command "foo" (in a pipeline or not), if Eshell finds a Lisp function for "foo", it always calls that function. On the other hand, "#'foo" always evaluates to the function object itself. So, since "cat" points to a Lisp function, your summary is pretty close, I'd say. However, I think I'd describe it as "cat is evaluated to hook up the pipeline into what the user meant". Currently, it works like so: 1. `eshell-parse-command' turns "echo hi | cat" into: (eshell-commands (progn (run-hooks 'eshell-pre-command-hook) (catch 'top-level (progn (eshell-trap-errors (eshell-execute-pipeline '((eshell-named-command "echo" (list "hi")) (eshell-named-command "cat")))))) (run-hooks 'eshell-post-command-hook))) 2. Eshell iteratively evaluates that, eventually getting to to the "cat" part 3. `eshell-named-command' looks for "cat" and calls the function `eshell/cat' 4. `eshell/cat' notices that it's being called in a pipeline and bails out, throwing `eshell-replace-command' to convert "cat" into "/bin/cat" (or wherever your cat program lives) 5. This gets reevaluated in `eshell-do-eval' and calls `eshell-external-command' for "/bin/cat" 6. This calls `eshell-gather-process-output', returning a process object with the pipes connected That is, Eshell evaluates `eshell/cat' (which could do anything, really) with the ultimate goal of hooking up the pipeline. Currently that just means "call /bin/cat and connect pipes to the external process", but `eshell/cat' can do whatever it wants to achieve the result. For the "pipe to a Lisp function" case, like "echo hi | #'upcase", what's actually happening under the hood in my patch it that "#'upcase" is evaluated, Eshell sees that the result is a function, and then the function is converted into a pseudo-pipe that gets properly hooked up to echo. For a case like "echo hi | less -N", where "less" has a Lisp implementation, Eshell evaluates `(eshell/less "-N")', which could return a function that gets converted into a pseudo-pipe like above. Or (and this is how `eshell/less' will actually work), it returns its own pseudo-pipe that's hooked up properly; this case is analogous to `eshell-gather-process-output' returning a properly-connected process object. The subtleties of Lisp functions in pipelines will probably be a bit easier to understand once I post patches, which should hopefully be this week or next. :) The patches work already, but I'm still implementing a few more pipeable Eshell commands in Lisp so that I can be sure the core implementation is flexible enough to be used for practical cases. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols 2022-01-25 20:52 ` Jim Porter @ 2022-01-25 22:38 ` Sean Whitton 0 siblings, 0 replies; 60+ messages in thread From: Sean Whitton @ 2022-01-25 22:38 UTC (permalink / raw) To: Jim Porter, 53518; +Cc: Michael Albinus Hello, On Tue 25 Jan 2022 at 12:52PM -08, Jim Porter wrote: > The subtleties of Lisp functions in pipelines will probably be a bit > easier to understand once I post patches, which should hopefully be this > week or next. :) The patches work already, but I'm still implementing a > few more pipeable Eshell commands in Lisp so that I can be sure the core > implementation is flexible enough to be used for practical cases. Thank you for the explanation. I still don't see how your proposal is a natural extension of what Eshell already does, to be honest, but I'm sure things will become clearer once you post your docs and tests, as you say. -- Sean Whitton ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-25 2:39 ` Jim Porter 2022-01-25 5:33 ` bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols Sean Whitton 2022-01-25 8:50 ` Sean Whitton @ 2022-01-25 8:54 ` Michael Albinus 2022-01-25 18:22 ` Jim Porter 2 siblings, 1 reply; 60+ messages in thread From: Michael Albinus @ 2022-01-25 8:54 UTC (permalink / raw) To: Jim Porter; +Cc: 46351, Lars Ingebrigtsen, Robert Pluim, Sean Whitton Jim Porter <jporterbugs@gmail.com> writes: Hi Jim, > #'upcase > > Eshell explicitly supports this construct (see `eshell-lisp-regexp'), > though it doesn't appear to be documented in the manual. Shouldn't it? Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2022-01-25 8:54 ` bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Michael Albinus @ 2022-01-25 18:22 ` Jim Porter 0 siblings, 0 replies; 60+ messages in thread From: Jim Porter @ 2022-01-25 18:22 UTC (permalink / raw) To: Michael Albinus; +Cc: 46351, Lars Ingebrigtsen, Robert Pluim, Sean Whitton On 1/25/2022 12:54 AM, Michael Albinus wrote: > Jim Porter <jporterbugs@gmail.com> writes: > > Hi Jim, > >> #'upcase >> >> Eshell explicitly supports this construct (see `eshell-lisp-regexp'), >> though it doesn't appear to be documented in the manual. > > Shouldn't it? Do you mean "shouldn't it be documented in the manual"? If so, I agree. I'll be sure to explain this in the manual alongside my patch to add support for piping to Lisp functions in Eshell. (It seems better to do it all at once since if I updated the manual now, I'd probably have to update those parts again to account for the pipeline changes.) ^ permalink raw reply [flat|nested] 60+ messages in thread
* bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining 2021-12-27 18:13 ` Sean Whitton 2021-12-27 18:22 ` Eli Zaretskii @ 2021-12-27 18:26 ` Michael Albinus 1 sibling, 0 replies; 60+ messages in thread From: Michael Albinus @ 2021-12-27 18:26 UTC (permalink / raw) To: Sean Whitton; +Cc: 46351 Sean Whitton <spwhitton@spwhitton.name> writes: > Hello Michael, Hi Sean, >> Again, I don't see the need for the heading ||. When there is a *| in >> the command line, DTRT. > > What I had in mind was a pipeline involving more than two commands, e.g. > > foo | bar | baz >quux > > Indeed, this is exactly the sort of pipeline you might want this feature > for. But the user would have to manually insert three asterisks to turn > on this feature. That seems pretty inconvenient -- any thoughts on how > to improve it? One to rule them all. As soon there is one *|, *< or *> in the command line, everything is regarded external. Also commands like ls or cat, which behave then like *ls or *cat. Best regards, Michael. ^ permalink raw reply [flat|nested] 60+ messages in thread
end of thread, other threads:[~2022-01-26 13:13 UTC | newest] Thread overview: 60+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2021-02-06 20:06 bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Sean Whitton 2021-02-07 9:17 ` Michael Albinus 2021-02-07 19:01 ` Sean Whitton 2021-02-08 10:28 ` Michael Albinus 2021-02-08 18:07 ` Sean Whitton 2021-02-10 11:33 ` Michael Albinus 2021-12-24 21:20 ` Sean Whitton 2021-12-25 13:51 ` Michael Albinus 2021-12-25 22:45 ` Sean Whitton 2021-12-27 14:42 ` Michael Albinus 2021-12-27 18:13 ` Sean Whitton 2021-12-27 18:22 ` Eli Zaretskii 2021-12-27 19:21 ` Sean Whitton 2021-12-27 19:35 ` Eli Zaretskii 2021-12-27 19:53 ` Sean Whitton 2021-12-27 19:37 ` Michael Albinus 2021-12-27 19:54 ` Sean Whitton 2021-12-28 8:58 ` Michael Albinus 2022-01-18 5:19 ` Sean Whitton 2022-01-18 9:49 ` Robert Pluim 2022-01-18 18:27 ` Sean Whitton 2022-01-18 14:45 ` Eli Zaretskii 2022-01-18 18:40 ` Sean Whitton 2022-01-18 19:38 ` Eli Zaretskii 2022-01-18 23:16 ` Sean Whitton 2022-01-19 7:34 ` Eli Zaretskii 2022-01-19 20:39 ` Sean Whitton 2022-01-20 6:53 ` Eli Zaretskii 2022-01-20 22:16 ` Sean Whitton 2022-01-21 6:54 ` Eli Zaretskii 2022-01-22 0:16 ` Sean Whitton 2022-01-18 18:42 ` Sean Whitton 2022-01-19 15:52 ` Michael Albinus 2022-01-19 20:54 ` Sean Whitton 2022-01-20 18:41 ` Michael Albinus 2022-01-20 22:17 ` Sean Whitton 2022-01-23 22:39 ` Sean Whitton 2022-01-24 9:55 ` Lars Ingebrigtsen 2022-01-24 14:18 ` Michael Albinus 2022-01-24 20:32 ` Sean Whitton 2022-01-24 20:44 ` Sean Whitton 2022-01-24 20:48 ` Lars Ingebrigtsen 2022-01-24 21:42 ` Sean Whitton 2022-01-24 21:51 ` Lars Ingebrigtsen 2022-01-24 22:48 ` Sean Whitton 2022-01-24 20:46 ` Lars Ingebrigtsen 2022-01-25 2:39 ` Jim Porter 2022-01-25 5:33 ` bug#53518: 29.0.50; em-extpipe breaks input of sharp-quoted Lisp symbols Sean Whitton 2022-01-25 8:50 ` Sean Whitton 2022-01-25 12:26 ` Lars Ingebrigtsen 2022-01-25 16:48 ` Sean Whitton 2022-01-26 5:38 ` Jim Porter 2022-01-26 13:13 ` Lars Ingebrigtsen 2022-01-25 18:14 ` Jim Porter 2022-01-25 20:01 ` Sean Whitton 2022-01-25 20:52 ` Jim Porter 2022-01-25 22:38 ` Sean Whitton 2022-01-25 8:54 ` bug#46351: 28.0.50; Add convenient way to bypass Eshell's own pipelining Michael Albinus 2022-01-25 18:22 ` Jim Porter 2021-12-27 18:26 ` Michael Albinus
Code repositories for project(s) associated with this public inbox https://git.savannah.gnu.org/cgit/emacs.git This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).