Making Shell Scripts Executable Just-in-Time
A different take on adding exec permissions to shell script in Emacs
In my work I often need to write small programs or scripts that
accomplish very specific tasks. Many of these involve fetching and
analyzing data from JSON-based APIs, and I tend to use shell scripts
for that sorta thing. I mostly rely on curl
, jq
, and parallel
,
as well as the other usual suspects grep
, sed
, tr
, head
,
sort
, uniq
, and the occasional awk
.
I usually begin writing these scripts as one-liners in an Emacs Shell
buffer before moving to a .sh
file. There I can generalize that
one-liner and format it nicely, and at some point also test it. Of
course, to test it I need to run it, and that requires giving the
newly created shell script executable permissions.
In Emacs, there are quite a few ways one can go about making their
script executable. You can, for instance, do M-! M-n chmod +x RET
,
but if you ask me that’s too much typing for such a common task!
Worse, it’s not very Emacsy either. Instead, the way I’ve been
doing this for a long time was jumping to Dired with C-x C-j
, and
immediately typing M +x RET
to make the file executable. That works
well and doesn’t require me to type chmod
, but it still takes some
typing and–crucially–it forces me to switch to Dired, when I really
just wanted was to test my shell script from its own buffer.
Recently, after going through that flow one too many times, I figured
there must be a better way. Ideally I would have simply liked to
hit C-c C-x
(AKA executable-interpret
) in my shell script buffer
without the need to explicitly make it executable beforehand. I
wasn’t surprised to quickly discover that Emacs comes with a dedicated
solution for this problem built-in. Typing C-h f
followed by exec
file TAB
lists among the completion candidates a function
executable-make-buffer-file-executable-if-script-p
that, as its
lengthy name suggest, makes the visited file executable if it happens
to be a script.
The standard piece of advice regarding this function, that you find
written throughout the interwebs by Emacs users and developers alike,
is that one should put this function in their after after-save-hook
.
In 2003, Stefan Monnier wrote on the emacs-devel mailing list:
we have make-buffer-file-executable-if-script-p and I recommend everybody add it to his after-save-hook.
This solves the problem of manually changing a shell script’s permissions prior to running it, because the script is made executable the second you save it–and saving the shell script buffer to a file is anyway a prerequisite for running it.
In the 20 years that passed since Stefan brought up that nifty trick, this advice was echoed in many esteemed Emacs blogs:
- Micky Petersen suggested it in his Mastering Emacs book,
- Marcin Borkowski mentioned it among other random Emacs tips,
- Oleh Krehel included it in his Emacs as system-wide Rhythmbox post, and
- Bozhidar Batsov wrote about it on his blog as well.
Yet, the function itself predates Stefan’s comment. It appears that
originally Noah Friedman wrote it all the way back in the year 2000
when it was added to Emacs by Dave Love–tracing the function’s
history by going to its definition in executable.el
and hitting M-h
C-x v h
reveals the following commit:
commit 778e1d17edb36cc53fd7419436311f2e2bc622ff Author: Dave Love <[email protected]> Date: Fri Jun 9 09:38:58 2000 +0000 ... (make-buffer-file-executable-if-script-p): New function from Noah Friedman. diff --git a/lisp/progmodes/executable.el b/lisp/progmodes/executable.el --- a/lisp/progmodes/executable.el +++ b/lisp/progmodes/executable.el @@ -264,1 +270,16 @@ - +(defun make-buffer-file-executable-if-script-p () + "Make file executable according to umask if not already executable. +If file already has any execute bits set at all, do not change existing +file modes." + (and (save-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (save-match-data + (looking-at "^#!")))) + (let* ((current-mode (file-modes (buffer-file-name))) + (add-mode (logand ?\111 (default-file-modes)))) + (or (/= (logand ?\111 current-mode) 0) + (zerop add-mode) + (set-file-modes (buffer-file-name) + (logior current-mode add-mode))))))
As we see in the above patch, back in 2000 this function would simply
look at the start of your buffer, and if it finds there a shebang
it’d ensure that the file has executable permissions. Other than the
name of the function becoming yet a little longer (the executable-
prefix was added, so to follow Elisp namespacing conventions), not
much has changed in terms of its implementation since then.
Although, as I described earlier, putting
executable-make-buffer-file-executable-if-script-p
into one’s
after-save-hook
is a practice promoted by many esteemed members of
the Emacs community (heck, it even made it to Sacha Chua’s config), I
felt uneasy about this solution. I save lots of files, and only a
minuscule fraction of them are scripts that I wanna make executable.
That means that the vast majority of my save operations will involve
some futile busywork and incur a (tiny, but still) needless
performance penalty. I also think that the way this function decides
whether or not to make a file executable is too coarse. It doesn’t
examine the file’s extension for example, nor does it take into
account the buffer’s major mode. Is it always TRT to make every file
that starts with a #
and a !
executable? I’m not sure, really.
It’s probably fine, but it makes my security-spidey-sense tingle
nonetheless.
The solution I came up with is both more conservative (no chance of
random files becoming executable against my wishes) and more efficient
(no penalizing all file saves for a few odd scripts). Instead of
adding executable-make-buffer-file-executable-if-script-p
to my
after after-save-hook
, I settled on adding it as a advice to the
command executable-interpret
. Here’s the relevant excerpt from
my config:
(with-eval-after-load 'executable (define-advice executable-interpret (:before (&rest _) ensure-executable) (unless (file-exists-p buffer-file-name) (basic-save-buffer)) (executable-make-buffer-file-executable-if-script-p)))
This :before
advice takes care of making the visited executable
exactly when I’m trying to actually execute it–just in time.