Emacs workflow to edit Bash scripts while they run
Bash can give unexpected results if one edits a script while the script is running. But it would often be very convenient to be able to edit a temporary copy of a script that I run within a shell, but make sure it is back in the original location before I run it again. Does anyone have suggestions for a workflow or customizations to Emacs to facilitate this?
M-x delete-file, press down to insert obtain the path to the file you're editing, and RET to delete the file. When you next save your buffer, this will create a new file. Bash will keep executing the old, deleted file.
By the way, in most cases, you don't even need to take this precaution. Emacs usually saves to a temporary file, then renames that temporary file to the proper file name (which deletes the old file, unless it's being kept as a backup). Emacs only overwrites the file in place if it detects that it can't recreate the file properly: if the file has hard links, or if you don't have write permission on the directory. (I'm simplifying a little; the details are in the basic-save-buffer-2 function in files.el.) As long as the file has a single directory entry and you have write permissions on the directory containing the script, just press C-x C-s.
In this reply, I
- supplement the nice answer by Gilles with the theory.
- suggest an improvment in emacs usage.
- suggest alternatives, purely by shell scripting.
Warning: I'm not a professional programmer.
The reason deletion is safe but modification is not is that in Unix, a deleted file gets inaccessible from the file system, but the processes that had opened the file (here, /bin/bash) can still read it, as explained here. It's not that bash does something special, like buffering the entire file, but it simply reads. (Even after the deletion, you can recover the script while it's running, as explained here. In bash, the script seems to be always opened as 255, /proc/<pid>/fd/255.)
2 Emacs solution
Now emacs; to be safe, you can automatize all; add to before-save-hook a function to check if it's sh-mode, and delete automatically if the script is open, by checking with lsof:
(defvar delete-open-sh-flag nil "Used by `delete-open-sh.` If nil, deletion didn't happen. Otherwise, the mode is saved.") (defun delete-open-sh () (setq delete-open-sh-flag nil) (when (and (eq major-mode 'sh-mode) (buffer-file-name) (file-exists-p (buffer-file-name))) (let ((buf (generate-new-buffer " *foo*"))) (call-process "lsof" nil; infile buf ; dest nil ; display (buffer-file-name)) (unless (eq 0 (buffer-size buf)) (setq delete-open-sh-flag (file-modes (buffer-file-name))) (delete-file (buffer-file-name))) (kill-buffer buf)))) (defun delete-open-sh-after () (when delete-open-sh-flag (set-file-modes (buffer-file-name) delete-open-sh-flag))) (add-hook 'before-save-hook 'delete-open-sh) (add-hook 'after-save-hook 'delete-open-sh-after)
But this is not perfect. It saves the file mode, but that's all.
3 Pure bash solution
Or, you can let the bash script itself do the deletion. Use this:
# Usage: . /path/to/this-script (Always use abs path) # Restart the caller script itself in a temporary file; this allows # editing of the caller script file while it's run. # Temporary file is soon deleted. # if [[ ! "`dirname $0`" =~ ^/tmp/.sh-tmp ]]; then mkdir -p /tmp/.sh-tmp/ DIST="/tmp/.sh-tmp/$( basename $0 )" install -m 700 "$0" $DIST exec $DIST "$@" else rm "$0" fi
Usage example: save this file in /home/foo/a.sh:
#!/bin/sh echo "$0 $@" . /home/foo/the-above-code.sh echo "Again, $0 $@"
If you invoke the above script as ./a.sh 1 2 3, the result is:
/home/foo/a.sh 1 2 3 /tmp/.sh-tmp/a.sh 1 2 3 Again, /tmp/.sh-tmp/a.sh 1 2 3
This might be the best, but use at your own risk.
It's also known that putting all in a brace, concluding with exit does the job, as explained here.