From 6fd2b6d40597d8b969086609d9b1266c1861fb1f Mon Sep 17 00:00:00 2001 From: Pierre Neidhardt Date: Wed, 8 Nov 2017 17:39:44 +0100 Subject: [PATCH] eshell: Make eshell-detach-send-input work on any valid bash command --- .emacs.d/lisp/init-eshell.el | 6 + .emacs.d/lisp/package-eshell-detach.el | 177 +++++++++++++++++-------- 2 files changed, 130 insertions(+), 53 deletions(-) diff --git a/.emacs.d/lisp/init-eshell.el b/.emacs.d/lisp/init-eshell.el index 49283983..8b1ca1c7 100644 --- a/.emacs.d/lisp/init-eshell.el +++ b/.emacs.d/lisp/init-eshell.el @@ -260,4 +260,10 @@ See `eshell' for the numeric prefix ARG." (add-hook 'eshell-pre-command-hook 'eshell-status-record) +;;; Detach +(when (require 'package-eshell-detach nil t) + (defun eshell-detach-set-keys () + (define-key eshell-mode-map (kbd "S-") 'eshell-detach-send-input)) + (add-hook 'eshell-mode-hook 'eshell-detach-set-keys)) + (provide 'init-eshell) diff --git a/.emacs.d/lisp/package-eshell-detach.el b/.emacs.d/lisp/package-eshell-detach.el index 667d6d48..373688e1 100644 --- a/.emacs.d/lisp/package-eshell-detach.el +++ b/.emacs.d/lisp/package-eshell-detach.el @@ -13,96 +13,167 @@ ;; emacs --batch --eval '(progn (eshell) (insert "echo hello") (eshell-send-input))' ;; Issues: --batch sends to stderr. How do we redirect the output to the real stdout/stderr? -;;; TODO: Rename eshell-detach and hide implementation details around "dtach". +(defvar eshell-detach-program "dtach" + "The `dtach' program.") -(defvar eshell-dtach-redraw-method nil +(defvar eshell-detach-redraw-method nil "If nil, use the default value. Value must be a string. See dtach(1) for possible values.") -(defvar eshell-dtach-shell "bash" +(defvar eshell-detach-shell "bash" "Shell to run the command in. Should be bash-compatible. The end command will be - \"`eshell-dtach-shell' -c { { ; } > >(tee stdout) } 2> >(tee stderr) | tee stdout+stderr\"") + \"`eshell-detach-shell' -c { { ; } > >(tee stdout) } 2> >(tee stderr) | tee stdout+stderr\"") -(defvar eshell-dtach-detach-character "^\\" +;; TODO: Set the detach character? +(defvar eshell-detach-detach-character "^\\" "Charcter to press to detach dtach, i.e. leave the process run in the background. The character syntax follows terminal notations, not Emacs.") -(defvar eshell-dtach-detach-character-binding "C-\\" - "The Emacs binding matching `eshell-dtach-detach-character'.") +(defvar eshell-detach-detach-character-binding "C-\\" + "The Emacs binding matching `eshell-detach-detach-character'.") -(defvar eshell-dtach-stdout-ext ".stdout" +(defvar eshell-detach-stdout-ext ".stdout" "If non-nil and a string, stdout will also be saved to file named after the socket with this extension appened. The 'tee' program is required.") -(defvar eshell-dtach-stderr-ext ".stderr" +(defvar eshell-detach-stderr-ext ".stderr" "If non-nil and a string, stderr will also be saved to file named after the socket with this extension appened. The 'tee' program is required.") -(defvar eshell-dtach-stdout+stderr-ext ".stdout+stderr" +(defvar eshell-detach-stdout+stderr-ext ".stdout+stderr" "If non-nil and a string, stdout and stderr will also be saved to file named after the socket with this extension appened. The 'tee' program is required.") -;; TODO: Make it work with pipes. -;; TODO: Test with quotes. -;; TODO: Hook should be on named commands as it should not run for Elisp code. -;; `eshell-named-command-hook' seems to be the way to go. What about +(defvar eshell-detach-directory (if server-socket-dir server-socket-dir temporary-file-directory) + "The directory where to store the dtach socket and the logs.") + +;; `eshell-named-command-hook' is not the way to go as it won't take pipelines. What about ;; `eshell-rewrite-command-hook'? -(defun eshell-detach-send-input-function (command args) - "If no other rewriting rule transforms TERMS, assume a named command." - (message "ESHELL %s %S" command args) - (remove-hook 'eshell-named-command-hook 'eshell-detach-send-input-function) - ;; Since sockets get killed on termination, there won't be any leftover if there is no log. Then it could be useful to _not_ create a subdir. Let's not do it then. - ;; TODO: Do not create log if empty. Workaround: delete 0-byte files. - (let* ((temporary-file-directory (if server-socket-dir server-socket-dir temporary-file-directory)) - ;; TODO: temp-file should not exist for dtach to start? That forces us to use make-temp-file, there could be a race condition. +(defun eshell-detach-rewrite-input (input) + "Rewrite INPUT so that it is ready for detaching." + ;; Since sockets get killed on termination, there won't be any leftover if + ;; there is no log. Thus it is cleaner to _not_ create a sub-directory. + ;; `tee' creates log files even if nothing is output. We cleanup on exit by + ;; deleting 0-byte files. + (let* ( + ;; TODO: temp-file should not exist for dtach to start? That forces us + ;; to use make-temp-file which is vulnerable to race condition. (socket (make-temp-name (expand-file-name (concat "dtach-" - (replace-regexp-in-string "[^A-Za-z0-9]" "_" (concat command "_" (mapconcat 'identity args "_"))) + (replace-regexp-in-string "[^A-Za-z0-9=-]" "_" input) "-" (format-time-string "%F-%R:%S") "-") - temporary-file-directory))) - - (stdout (if eshell-dtach-stdout-ext (concat socket eshell-dtach-stdout-ext) nil)) - (stderr (if eshell-dtach-stderr-ext (concat socket eshell-dtach-stderr-ext) nil)) - (stdout+stderr (if eshell-dtach-stdout+stderr-ext (concat socket eshell-dtach-stdout+stderr-ext) nil)) - + eshell-detach-directory))) + (stdout (if eshell-detach-stdout-ext (concat socket eshell-detach-stdout-ext) nil)) + (stderr (if eshell-detach-stderr-ext (concat socket eshell-detach-stderr-ext) nil)) + (stdout+stderr (if eshell-detach-stdout+stderr-ext (concat socket eshell-detach-stdout+stderr-ext) nil)) ;; The following test command was inspired by ;; https://stackoverflow.com/questions/21465297/tee-stdout-and-stderr-to-separate-files-while-retaining-them-on-their-respective ;; { { echo stdout; echo stderr >&2; } > >(tee stdout.txt ); } 2> >(tee stderr.txt ) | tee stdout+stderr.txt - - ;; TODO: Use 'tee -a', cause then we don't - ;; lose anything when running in the background. If stdout and stderr can refer - ;; to the same files. Or can they? Yes, with 'tee -a' - (commandline (format "{ { %s; }%s }%s %s" - (concat command " " (mapconcat 'identity args " ")) + (commandline (format "{ { %s; }%s }%s %s; for i in %s %s %s; do [ ! -s \"$i\" ] && rm -- \"$i\"; done" + input (if stdout (format " > >(tee %s );" stdout) "") (if stderr (format " 2> >(tee %s )" stderr) "") - (if stdout+stderr (format " | tee %s" stdout+stderr) "")))) - (message "%s" - (list - "dtach" - "-c" socket "-z" - eshell-dtach-shell "-c" commandline)) - (throw 'eshell-replace-command - (eshell-parse-command "dtach" (cons "-c" - (list - socket "-z" - eshell-dtach-shell "-c" commandline)))))) + (if stdout+stderr (format " | tee %s" stdout+stderr) "") + (shell-quote-argument (or stdout "")) + (shell-quote-argument (or stderr "")) + (shell-quote-argument (or stdout+stderr ""))))) + (format "%s -c %s -z %s -c %s" eshell-detach-program socket eshell-detach-shell (shell-quote-argument commandline)))) -(defun eshell-detach-send-input () - (interactive) - (add-hook 'eshell-named-command-hook 'eshell-detach-send-input-function) - (eshell-send-input)) +;;; This is almost an exact copy of `eshell-send-input'. +(defun eshell-detach-send-input (&optional use-region queue-p no-newline) + "Send the input received to Eshell for parsing and processing. +After `eshell-last-output-end', sends all text from that marker to +point as input. Before that marker, calls `eshell-get-old-input' to +retrieve old input, copies it to the end of the buffer, and sends it. -;;; TODO: See if `make-process' is the way to go: it supports stderr/stdout separation and stop/cont. +If USE-REGION is non-nil, the current region (between point and mark) +will be used as input. -;;; TODO: Re-use `eshell-gather-process-output'? Re-implement? +If QUEUE-P is non-nil, input will be queued until the next prompt, +rather than sent to the currently active process. If no process, the +input is processed immediately. -;; TODO: But how to pause/resume? +If NO-NEWLINE is non-nil, the input is sent without an implied final +newline." + (interactive "P") + ;; Note that the input string does not include its terminal newline. + (let ((proc-running-p (and (eshell-interactive-process) + (not queue-p))) + (inhibit-point-motion-hooks t) + (inhibit-modification-hooks t)) + (unless (and proc-running-p + (not (eq (process-status + (eshell-interactive-process)) + 'run))) + (if (or proc-running-p + (>= (point) eshell-last-output-end)) + (goto-char (point-max)) + (let ((copy (eshell-get-old-input use-region))) + (goto-char eshell-last-output-end) + (insert-and-inherit copy))) + (unless (or no-newline + (and eshell-send-direct-to-subprocesses + proc-running-p)) + (insert-before-markers-and-inherit ?\n)) + (if proc-running-p + (progn + (eshell-update-markers eshell-last-output-end) + (if (or eshell-send-direct-to-subprocesses + (= eshell-last-input-start eshell-last-input-end)) + (unless no-newline + (process-send-string (eshell-interactive-process) "\n")) + (process-send-region (eshell-interactive-process) + eshell-last-input-start + eshell-last-input-end))) + (if (= eshell-last-output-end (point)) + (run-hooks 'eshell-post-command-hook) + (let (input) + (eshell-condition-case err + (progn + (setq input (buffer-substring-no-properties + eshell-last-output-end (1- (point)))) + (run-hook-with-args 'eshell-expand-input-functions + eshell-last-output-end (1- (point))) + (let ((cmd + ;; TODO: This is the modification. Report upstream the + ;; lack of flexibility. + ;; (eshell-parse-command-input + ;; eshell-last-output-end (1- (point))))) + (eshell-parse-command + (eshell-detach-rewrite-input input) nil t))) + (when cmd + (eshell-update-markers eshell-last-output-end) + (setq input (buffer-substring-no-properties + eshell-last-input-start + (1- eshell-last-input-end))) + (run-hooks 'eshell-input-filter-functions) + (and (catch 'eshell-terminal + (ignore + (if (eshell-invoke-directly cmd) + (eval cmd) + (eshell-eval-command cmd input)))) + (eshell-life-is-too-much))))) + (quit + (eshell-reset t) + (run-hooks 'eshell-post-command-hook) + (signal 'quit nil)) + (error + (eshell-reset t) + (eshell-interactive-print + (concat (error-message-string err) "\n")) + (run-hooks 'eshell-post-command-hook) + (insert-and-inherit input))))))))) + +;;; TODO: Remove bash / tee / dtach dependencies. +;;; See if `make-process' is the way to go: it supports stderr/stdout separation and stop/cont. +;;; Re-use `eshell-gather-process-output'? Re-implement? + +;; TODO: Pause/resume on Eshell. ;; Bash is one way: ;; (local-set-key (kbd "C-z") 'self-insert-command) ;; Pressing self-inserted "C-z RET" works.