Allow using multiple buffers in 'eshell-command'

Provide the same functionality as 'async-shell-command-buffer' but for
'eshell-command'.

Co-Authored-By: Jim Porter <jporterbugs@gmail.com>

* lisp/eshell/eshell.el (eshell-command-async-buffer): New option...
(eshell-command): ... use it.

* lisp/eshell/esh-proc.el (eshell-sentinel): Check for buffer liveness
in 'finish-io'.

* test/lisp/eshell/eshell-tests.el
(eshell-test/eshell-command/output-buffer/async-kill): New test.

* etc/NEWS: Announce this change (bug#71554).
This commit is contained in:
Thierry Volpiatto 2024-06-19 12:02:59 +02:00 committed by Jim Porter
parent bd86a6c4fd
commit 7f631a3e2a
4 changed files with 135 additions and 35 deletions

View file

@ -43,6 +43,17 @@ applies, and please also update docstrings as needed.
If 'whitespace-style' includes 'missing-newline-at-eof (which is the
default), the 'whitespace-cleanup' function will now add the newline.
** Eshell
---
*** New option 'eshell-command-async-buffer'.
This option lets you tell 'eshell-command' how to respond if its output
buffer is already in use by another invocation of 'eshell-command', much
like 'async-shell-command-buffer' does for 'shell-command'. By default,
this will prompt for confirmation before creating a new buffer when
necessary. To restore the previous behavior, set this option to
'confirm-kill-process'.
** SHR
+++

View file

@ -530,30 +530,34 @@ PROC is the process that's exiting. STRING is the exit message."
(not (process-live-p proc))))
(finish-io
(lambda ()
(with-current-buffer (process-buffer proc)
(if (or (process-get proc :eshell-busy)
(and wait-for-stderr (car stderr-live)))
(progn
(if (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(if (or (process-get proc :eshell-busy)
(and wait-for-stderr (car stderr-live)))
(progn
(eshell-debug-command 'process
"i/o busy for process `%s'" proc)
(run-at-time 0 nil finish-io))
(when data
(ignore-error eshell-pipe-broken
(eshell-output-object
data index handles)))
(eshell-close-handles
status
(when status (list 'quote (= status 0)))
handles)
;; Clear the handles to mark that we're 100%
;; finished with the I/O for this process.
(process-put proc :eshell-handles nil)
(eshell-debug-command 'process
"i/o busy for process `%s'" proc)
(run-at-time 0 nil finish-io))
(when data
(ignore-error eshell-pipe-broken
(eshell-output-object
data index handles)))
(eshell-close-handles
status
(when status (list 'quote (= status 0)))
handles)
;; Clear the handles to mark that we're 100%
;; finished with the I/O for this process.
(process-put proc :eshell-handles nil)
(eshell-debug-command 'process
"finished external process `%s'" proc)
(if primary
(run-hook-with-args 'eshell-kill-hook
proc string)
(setcar stderr-live nil)))))))
"finished external process `%s'" proc)
(if primary
(run-hook-with-args 'eshell-kill-hook
proc string)
(setcar stderr-live nil))))
(eshell-debug-command 'process
"buffer for external process `%s' already killed"
proc)))))
(funcall finish-io)))
(when-let ((entry (assq proc eshell-process-list)))
(eshell-remove-process-entry entry))))))

View file

@ -216,6 +216,34 @@ named \"*eshell*<2>\"."
:type 'string
:group 'eshell)
(defcustom eshell-command-async-buffer 'confirm-new-buffer
"What to do when the output buffer is used by another shell command.
This option specifies how to resolve the conflict where a new command
wants to direct its output to the buffer whose name is stored
in `eshell-command-buffer-name-async', but that buffer is already
taken by another running shell command.
The value `confirm-kill-process' is used to ask for confirmation before
killing the already running process and running a new process in the
same buffer, `confirm-new-buffer' for confirmation before running the
command in a new buffer with a name other than the default buffer name,
`new-buffer' for doing the same without confirmation,
`confirm-rename-buffer' for confirmation before renaming the existing
output buffer and running a new command in the default buffer,
`rename-buffer' for doing the same without confirmation."
:type '(choice (const :tag "Confirm killing of running command"
confirm-kill-process)
(const :tag "Confirm creation of a new buffer"
confirm-new-buffer)
(const :tag "Create a new buffer"
new-buffer)
(const :tag "Confirm renaming of existing buffer"
confirm-rename-buffer)
(const :tag "Rename the existing buffer"
rename-buffer))
:group 'eshell
:version "31.1")
;;;_* Running Eshell
;;
;; There are only three commands used to invoke Eshell. The first two
@ -283,11 +311,19 @@ information on Eshell, see Info node `(eshell)Top'."
(eshell-command-mode +1))
(read-from-minibuffer prompt))))
(defvar eshell-command-buffer-name-async "*Eshell Async Command Output*")
(defvar eshell-command-buffer-name-sync "*Eshell Command Output*")
;;;###autoload
(defun eshell-command (command &optional to-current-buffer)
"Execute the Eshell command string COMMAND.
If TO-CURRENT-BUFFER is non-nil (interactively, with the prefix
argument), then insert output into the current buffer at point."
argument), then insert output into the current buffer at point.
When \"&\" is added at end of command, the command is async and its output
appears in a specific buffer. You can customize
`eshell-command-async-buffer' to specify what to do when this output
buffer is already taken by another running shell command."
(interactive (list (eshell-read-command)
current-prefix-arg))
(save-excursion
@ -301,18 +337,46 @@ argument), then insert output into the current buffer at point."
(eshell-current-subjob-p))
,(eshell-parse-command command))
command))
intr
(bufname (if (eq (car-safe proc) :eshell-background)
"*Eshell Async Command Output*"
(setq intr t)
"*Eshell Command Output*")))
(if (buffer-live-p (get-buffer bufname))
(kill-buffer bufname))
(rename-buffer bufname)
(async (eq (car-safe proc) :eshell-background))
(bufname (cond
(to-current-buffer nil)
(async eshell-command-buffer-name-async)
(t eshell-command-buffer-name-sync)))
unique)
(when bufname
(when (buffer-live-p (get-buffer bufname))
(cond
((with-current-buffer bufname
(and (null eshell-foreground-command)
(null eshell-background-commands)))
;; The old buffer is done executing; kill it so we can
;; take its place.
(kill-buffer bufname))
((eq eshell-command-async-buffer 'confirm-kill-process)
(shell-command--same-buffer-confirm "Kill it")
(with-current-buffer bufname
;; Stop all the processes in the old buffer (there may
;; be several).
(eshell-process-interact #'interrupt-process t))
(accept-process-output)
(kill-buffer bufname))
((eq eshell-command-async-buffer 'confirm-new-buffer)
(shell-command--same-buffer-confirm "Use a new buffer")
(setq unique t))
((eq eshell-command-async-buffer 'new-buffer)
(setq unique t))
((eq eshell-command-async-buffer 'confirm-rename-buffer)
(shell-command--same-buffer-confirm "Rename it")
(with-current-buffer bufname
(rename-uniquely)))
((eq eshell-command-async-buffer 'rename-buffer)
(with-current-buffer bufname
(rename-uniquely)))))
(rename-buffer bufname unique))
;; things get a little coarse here, since the desire is to
;; make the output as attractive as possible, with no
;; extraneous newlines
(when intr
(unless async
(apply #'eshell-wait-for-process (cadr eshell-foreground-command))
(cl-assert (not eshell-foreground-command))
(goto-char (point-max))
@ -320,7 +384,7 @@ argument), then insert output into the current buffer at point."
(delete-char -1)))
(cl-assert (and buf (buffer-live-p buf)))
(unless to-current-buffer
(let ((len (if (not intr) 2
(let ((len (if async 2
(count-lines (point-min) (point-max)))))
(cond
((= len 0)
@ -336,7 +400,7 @@ argument), then insert output into the current buffer at point."
;; cause the output buffer to take up as little screen
;; real-estate as possible, if temp buffer resizing is
;; enabled
(and intr temp-buffer-resize-mode
(and (not async) temp-buffer-resize-mode
(resize-temp-buffer-window)))))))))))
;;;###autoload

View file

@ -117,6 +117,27 @@ This test uses a pipeline for the command."
(forward-line)
(should (looking-at "hi\n"))))))
(ert-deftest eshell-test/eshell-command/output-buffer/async-kill ()
"Test that the `eshell-command' function kills the old process when told to."
(skip-unless (executable-find "echo"))
(ert-with-temp-directory eshell-directory-name
(let ((orig-processes (process-list))
(eshell-history-file-name nil)
(eshell-command-async-buffer 'confirm-kill-process))
(eshell-command "sleep 5 | *echo hi &")
(cl-letf* ((result t)
;; Say "yes" only once: for the `confirm-kill-process'
;; prompt. If there are any other prompts (e.g. from
;; `kill-buffer'), say "no" to make the test fail.
((symbol-function 'yes-or-no-p)
(lambda (_prompt) (prog1 result (setq result nil)))))
(eshell-command "*echo bye &"))
(eshell-wait-for (lambda () (equal (process-list) orig-processes)))
(with-current-buffer "*Eshell Async Command Output*"
(goto-char (point-min))
(forward-line)
(should (looking-at "bye\n"))))))
(ert-deftest eshell-test/command-running-p ()
"Modeline should show no command running"
(with-temp-eshell