Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 115 additions & 47 deletions shell-file.el
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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)))
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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