]> git.eshelyaron.com Git - emacs.git/commitdiff
Allow using multiple buffers in 'eshell-command'
authorThierry Volpiatto <thievol@posteo.net>
Wed, 19 Jun 2024 10:02:59 +0000 (12:02 +0200)
committerEshel Yaron <me@eshelyaron.com>
Sun, 7 Jul 2024 13:16:55 +0000 (15:16 +0200)
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).

(cherry picked from commit 7f631a3e2aca97e95b8659c902c25ab21f084e08)

etc/NEWS
lisp/eshell/esh-proc.el
lisp/eshell/eshell.el
test/lisp/eshell/eshell-tests.el

index 1af252e8a8f5c205be959b75cffc7de926d3ae40..3d2b86cfb6aad8abec69f8b315ba74df72562a7d 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -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
 
 +++
index 2ff41c3d409baf0cb0e72ff1f025317988eab8f4..a5e9de7990705ba7fb7fa2fc4fe11bba4d3856aa 100644 (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))))))
index 18e05a371a4d88b521dc796b9ca5031b1f5106a6..568f674506761c1b400e9841988cdb6fd7f12fe4 100644 (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
index e58b5a14ed9ce8513ae412653617791e05ec62a4..f16c28cd1ae66a27c6652d7dcd7990221da9d92d 100644 (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