Making Shell Scripts Executable Just-in-Time

A different take on adding exec permissions to shell script in Emacs

Created on [2023-04-08], last updated [2023-09-27]

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:

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

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)

This :before advice takes care of making the visited executable exactly when I’m trying to actually execute it–just in time.