diff --git a/shell-file.el b/shell-file.el index 9bedaa6..9919e1e 100644 --- a/shell-file.el +++ b/shell-file.el @@ -6,6 +6,10 @@ (defcustom shell-file-dir (format "/tmp/shell-file.%s" (user-login-name)) "Where shell-file commands and output will be logged") +(defcustom shell-file-remote-host nil + "Remote host to execute shell-file commands on. +Set this to a string like \"user@host\" to enable remote execution.") + (setq shell-file-init-line-re "^### START #########################.*\n") (setq shell-file-exit-line-re "^exit ##############################.*\n") @@ -68,7 +72,7 @@ exit ############################################################### "insert a new shell block on top of the shell-file" (interactive) (unless block-text - (when (evil-visual-state-p) + (when (and (featurep 'evil) (evil-visual-state-p)) (setq block-text (buffer-substring-no-properties (region-beginning) (region-end))) @@ -83,11 +87,15 @@ exit ############################################################### (when block-text (insert block-text))) ) +(defun shell-file-cd-command (dir) + "the cd command inserted into shell-file.sh" + (concat "cd " dir)) + (defun shell-file-insert-cd () "insert a new shell block on top of the shell-file" (interactive) (let* ((dir default-directory)) - (shell-file-insert-block (concat "cd " dir)))) + (shell-file-insert-block (shell-file-cd-command dir)))) (defun shell-file-insert-file () "insert a new shell block on top of the shell-file" @@ -97,7 +105,7 @@ exit ############################################################### (file-name-nondirectory (if (eq major-mode 'dired-mode) (dired-filename-at-point) (buffer-file-name))))) - (shell-file-insert-block (concat "cd " dir "\n./" file)))) + (shell-file-insert-block (concat (shell-file-cd-command dir) "\n./" file)))) (defun shell-file-delete-block (num-times) "delete a shell block off the top of the shell-file" @@ -135,6 +143,7 @@ exit ############################################################### (next-line) (let ((beg (point))) (search-forward exit-line) + (setq last-command nil) ; Don't append to the previous kill. (kill-region beg (point))) (goto-line 1) (search-forward init-line) @@ -186,61 +195,120 @@ exit ############################################################### (throw 'body (switch-to-buffer name))) (setq slot (+ slot 1))))))) + +(defun shell-file-log (message) + "Log a message to the *Messages* buffer." + (message "[Shell-File] %s" message)) + (defun shell-file-run () - "run shell-file top block in the background" + "Run shell-file top block in the background, locally or remotely." (interactive) (when (shell-file-current-buffer-is-block-buffer) (shell-file-bubble-block t)) - (let* - (;; compute log files - (dir shell-file-dir) - (_ (mkdir dir t)) - (uid (format-time-string "%Y-%m-%d.%H:%M:%S")) - (cmd (format "%s/%s.command" dir uid)) - (out (format "%s/%s.output" dir uid)) - ;; write out block - (_ - (save-window-excursion - (save-excursion - (shell-file-find) - (goto-char (point-min)) - (search-forward-regexp (shell-file-exit-line)) - (write-region (point-min) (match-end 0) cmd)))) - ;; execute block - (_ (chmod cmd - (file-modes-symbolic-to-number "u+x" (file-modes cmd)))) - (cmd (format "%s 2>&1 | tee %s" cmd out)) - (buf (save-window-excursion (shell-file-output))) - (_ (async-shell-command cmd buf)) - (_ (display-buffer buf nil 'visible)) - ) - nil)) + (let* ((remote-prefix (when shell-file-remote-host + (format "/ssh:%s:" shell-file-remote-host))) + (default-directory (if remote-prefix + (concat remote-prefix "/") + default-directory)) + (dir (if remote-prefix + (concat remote-prefix shell-file-dir) + shell-file-dir)) + (_ (make-directory dir t)) + (uid (format-time-string "%Y-%m-%d.%H:%M:%S")) + (out-file (expand-file-name (format "%s.output" uid) dir))) + + (shell-file-log "Preparing to run command...") + (shell-file-log (format "Remote host: %s" (or shell-file-remote-host "local"))) + (shell-file-log (format "Working directory: %s" default-directory)) + + (shell-file-write-command-file out-file) + (shell-file-execute-command out-file remote-prefix))) + +(defun shell-file-write-command-file (cmd-file) + "Write the shell-file command to the specified file, including the prelude and first block." + (shell-file-log (format "Writing command to %s" cmd-file)) + (save-window-excursion + (save-excursion + (shell-file-find) + (goto-char (point-min)) + (let ((block-end (progn + (search-forward-regexp (shell-file-init-line)) + (search-forward-regexp (shell-file-exit-line)) + (search-forward-regexp "^") + (match-beginning 0)))) + (let ((content (buffer-substring-no-properties (point-min) block-end))) + (write-region content nil cmd-file)))))) + +(defun shell-file-execute-command (out-file remote-prefix) + "Execute the shell-file command, locally or remotely." + (let* ((out-file-local (file-local-name out-file)) + (full-cmd (format "(sed -n %s %s; echo; script -qc 'bash -i %s' /dev/null 0<&0) 2>&1 | tee -a %s" + (shell-quote-argument + (concat "/" (replace-regexp-in-string "\n$" "" shell-file-init-line-re) "/,$p")) + (shell-quote-argument out-file-local) + (shell-quote-argument out-file-local) + (shell-quote-argument out-file-local)))) + (shell-file-log (format "Executing command: %s" full-cmd)) + (let ((buf (shell-file-output))) + (if remote-prefix + (let ((remote-cmd (format "ssh %s %s" + (shell-quote-argument (file-remote-p remote-prefix 'host)) + (shell-quote-argument full-cmd)))) + (shell-file-log (format "Remote command: %s" remote-cmd)) + (async-shell-command remote-cmd buf)) + (async-shell-command full-cmd buf)) + (display-buffer buf nil 'visible)) + (shell-file-log "Command execution initiated."))) + +(defun shell-file-set-remote-host (host) + "Set the remote host for shell-file execution." + (interactive "sEnter remote host (user@host): ") + (setq shell-file-remote-host (if (string-empty-p host) nil host)) + (message "Remote host set to %s" (or shell-file-remote-host "nil (local execution)"))) + +(defun shell-file-visit-outputs () + (interactive) + (dired shell-file-dir) + (dired-sort-other "-lt") ;; Sort by time, newest first. + (goto-char (point-min)) + (forward-line)) (defun shell-file-define-global-keys (map key-prefix) "add shell-file keybindings that should be available globally" (dolist (binding - '(("f" shell-file-find) - ("i" shell-file-insert-block) - ("x" shell-file-insert-file) - ("g" shell-file-insert-cd) - ("r" shell-file-run))) - (let* ((key (car binding)) - (key (concat key-prefix key)) - (def (cadr binding))) - (define-key map key def)))) + '((shell-file-find "f" "\C-f") + (shell-file-insert-block "i" "\C-i") + (shell-file-insert-file "x" "\C-x") + (shell-file-insert-cd "g" "\C-g") + (shell-file-run "r" "\C-r") + (shell-file-set-remote-host "h" "\C-h") + (shell-file-visit-outputs "o" "\C-o"))) + (let* ((def (car binding)) + (keys (cdr binding))) + (dolist (key keys) + (define-key map (concat key-prefix key) def))))) + +(defun define-shell-file-mode-key (key-prefix key def) + (let ((key (concat key-prefix key))) + (if (featurep 'evil) + (dolist (state (list 'normal 'motion)) + (evil-define-key state shell-file-mode-map key def)) + (define-key shell-file-mode-map key def)))) (defun shell-file-define-minor-mode-keys (key-prefix) "add shell-file keybindings that should be available in shell-file-mode" (dolist (binding - '(("b" shell-file-bubble-block) - ("d" shell-file-delete-block) - ("j" shell-file-next-block) - ("k" shell-file-prev-block) - ("s" shell-file-split-block))) - (let* ((key (car binding)) - (key (concat key-prefix key)) - (def (cadr binding))) - (dolist (state (list 'normal 'motion)) - (evil-define-key state shell-file-mode-map key def))))) + '((shell-file-bubble-block "b" "\C-b") + (shell-file-delete-block "d" "\C-d") + (shell-file-next-block "j" "n" "\C-j" "\C-n") + (shell-file-prev-block "k" "p" "\C-k" "\C-p") + (shell-file-split-block "s" "\C-s"))) + (let* ((def (car binding)) + (keys (cdr binding))) + (dolist (key keys) + (define-shell-file-mode-key key-prefix key def))))) + +(provide 'shell-file) +;;; shell-file ends here