eshell: Make eshell-detach-send-input work on any valid bash command

master
Pierre Neidhardt 2017-11-08 17:39:44 +01:00
parent b2a64fd4f6
commit 6fd2b6d405
2 changed files with 130 additions and 53 deletions

View File

@ -260,4 +260,10 @@ See `eshell' for the numeric prefix ARG."
(add-hook 'eshell-pre-command-hook 'eshell-status-record) (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-<return>") 'eshell-detach-send-input))
(add-hook 'eshell-mode-hook 'eshell-detach-set-keys))
(provide 'init-eshell) (provide 'init-eshell)

View File

@ -13,96 +13,167 @@
;; emacs --batch --eval '(progn (eshell) (insert "echo hello") (eshell-send-input))' ;; 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? ;; 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. "If nil, use the default value.
Value must be a string. Value must be a string.
See dtach(1) for possible values.") See dtach(1) for possible values.")
(defvar eshell-dtach-shell "bash" (defvar eshell-detach-shell "bash"
"Shell to run the command in. "Shell to run the command in.
Should be bash-compatible. Should be bash-compatible.
The end command will be The end command will be
\"`eshell-dtach-shell' -c { { <command>; } > >(tee stdout) } 2> >(tee stderr) | tee stdout+stderr\"") \"`eshell-detach-shell' -c { { <command>; } > >(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. "Charcter to press to detach dtach, i.e. leave the process run in the background.
The character syntax follows terminal notations, not Emacs.") The character syntax follows terminal notations, not Emacs.")
(defvar eshell-dtach-detach-character-binding "C-\\" (defvar eshell-detach-detach-character-binding "C-\\"
"The Emacs binding matching `eshell-dtach-detach-character'.") "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. "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.") 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. "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.") 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. "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.") The 'tee' program is required.")
;; TODO: Make it work with pipes. (defvar eshell-detach-directory (if server-socket-dir server-socket-dir temporary-file-directory)
;; TODO: Test with quotes. "The directory where to store the dtach socket and the logs.")
;; 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 ;; `eshell-named-command-hook' is not the way to go as it won't take pipelines. What about
;; `eshell-rewrite-command-hook'? ;; `eshell-rewrite-command-hook'?
(defun eshell-detach-send-input-function (command args) (defun eshell-detach-rewrite-input (input)
"If no other rewriting rule transforms TERMS, assume a named command." "Rewrite INPUT so that it is ready for detaching."
(message "ESHELL %s %S" command args) ;; Since sockets get killed on termination, there won't be any leftover if
(remove-hook 'eshell-named-command-hook 'eshell-detach-send-input-function) ;; there is no log. Thus it is cleaner to _not_ create a sub-directory.
;; 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. ;; `tee' creates log files even if nothing is output. We cleanup on exit by
;; TODO: Do not create log if empty. Workaround: delete 0-byte files. ;; deleting 0-byte files.
(let* ((temporary-file-directory (if server-socket-dir server-socket-dir temporary-file-directory)) (let* (
;; TODO: temp-file should not exist for dtach to start? That forces us to use make-temp-file, there could be a race condition. ;; 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 (socket (make-temp-name
(expand-file-name (expand-file-name
(concat "dtach-" (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") "-") "-" (format-time-string "%F-%R:%S") "-")
temporary-file-directory))) eshell-detach-directory)))
(stdout (if eshell-detach-stdout-ext (concat socket eshell-detach-stdout-ext) nil))
(stdout (if eshell-dtach-stdout-ext (concat socket eshell-dtach-stdout-ext) nil)) (stderr (if eshell-detach-stderr-ext (concat socket eshell-detach-stderr-ext) nil))
(stderr (if eshell-dtach-stderr-ext (concat socket eshell-dtach-stderr-ext) nil)) (stdout+stderr (if eshell-detach-stdout+stderr-ext (concat socket eshell-detach-stdout+stderr-ext) nil))
(stdout+stderr (if eshell-dtach-stdout+stderr-ext (concat socket eshell-dtach-stdout+stderr-ext) nil))
;; The following test command was inspired by ;; 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 ;; 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 ;; { { echo stdout; echo stderr >&2; } > >(tee stdout.txt ); } 2> >(tee stderr.txt ) | tee stdout+stderr.txt
(commandline (format "{ { %s; }%s }%s %s; for i in %s %s %s; do [ ! -s \"$i\" ] && rm -- \"$i\"; done"
;; TODO: Use 'tee -a', cause then we don't input
;; 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 " "))
(if stdout (format " > >(tee %s );" stdout) "") (if stdout (format " > >(tee %s );" stdout) "")
(if stderr (format " 2> >(tee %s )" stderr) "") (if stderr (format " 2> >(tee %s )" stderr) "")
(if stdout+stderr (format " | tee %s" stdout+stderr) "")))) (if stdout+stderr (format " | tee %s" stdout+stderr) "")
(message "%s" (shell-quote-argument (or stdout ""))
(list (shell-quote-argument (or stderr ""))
"dtach" (shell-quote-argument (or stdout+stderr "")))))
"-c" socket "-z" (format "%s -c %s -z %s -c %s" eshell-detach-program socket eshell-detach-shell (shell-quote-argument commandline))))
eshell-dtach-shell "-c" commandline))
(throw 'eshell-replace-command
(eshell-parse-command "dtach" (cons "-c"
(list
socket "-z"
eshell-dtach-shell "-c" commandline))))))
(defun eshell-detach-send-input () ;;; This is almost an exact copy of `eshell-send-input'.
(interactive) (defun eshell-detach-send-input (&optional use-region queue-p no-newline)
(add-hook 'eshell-named-command-hook 'eshell-detach-send-input-function) "Send the input received to Eshell for parsing and processing.
(eshell-send-input)) 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: ;; Bash is one way:
;; (local-set-key (kbd "C-z") 'self-insert-command) ;; (local-set-key (kbd "C-z") 'self-insert-command)
;; Pressing self-inserted "C-z RET" works. ;; Pressing self-inserted "C-z RET" works.