Tiny Shell Puzzle: Find all elisp files in my config which don't have a lexical-binding directive
For reasons unrelated to this post, the phrase -*- lexical-binding: t -*-
needs to exist as a comment in the first line of all my Emacs Lisp files.
This is what the first line of most of my elisp files looks like:
;;; init-isearch.el --- Configuring built-in isearch the way I like it
The line starts with ;;;
(the comment syntax in Elisp), followed by the name of the file, and then a description of what the file contains.
This line needs to change to the following:
;;; init-isearch.el --- Configuring built-in isearch the way I like it -*- lexical-binding: t -*-
The caveats are as follows:
- Some files already contain this directive, so they shouldn't be modified
- Some files do not start with
;;;
, these should be ignored.
With those caveats in place, here is the tiny shell puzzle:
- Find all the files which need to be modified.
- Modify all the files, writing the change back to disk
This is a great example of the kind of one-off tasks that shell scripting is great for!
Once you've given it a try, scroll down to see my answer.
Find all files which do not contain the phrase
I use ripgrep
for all my searching, and it gives me a handy way of solving this part of the problem:
rg -t elisp --files-without-match "lexical-binding"
Add the directive to the first line of each file
My mind immediately jumped to using awk
, which is my goto power tool for modifying file content. This problem is actually easier to solve with sed
, but I've dropped sed
entirely from my toolkit (in favour of awk
).
gawk '
(NR==1 && /^;;;/){ print $0 " -*- lexical-binding: t -*-" };
(NR==1 && !/^;;;/){ print $0 };
(NR>1){ print $0 };
'
The final answer, with an explanation of what each line does
1 rg -t elisp --files-without-match "lexical-binding" | \
2 xargs -I {} \
3 gawk -i inplace '
4 (NR==1 && /^;;;/){ print $0 " -*- lexical-binding: t -*-" };
5 (NR==1 && !/^;;;/){ print $0 };
6 (NR>1){ print $0 };
7 ' {}
The meaning of each line is:
- Line 1: Use
rg
to find all fines which do not containlexical-binding
, and print only filenames. - Line 2: Pipe the filenames to
xargs
for pushing one-by-one toawk
. - Line 3: Use the
-i inplace
switch to edit the file instead of printing to stdout. - Line 4: If line number is 1 and the line starts with
;;;
, add-*- lexical-binding: t -*-
to the end of the line. - Line 5: If line number is 1 but the line does not start with
;;;
, just print the line as-is. - Line 6: For all other lines, just print the line as-is.
- Line 7: This is where
xargs
will punch in the filename from our search (replacing{}
with the filename)
There is a bug in the first line – our search query. Can you spot it? See if you can, and then scroll on to see the answer
Searching only in the first line of the file
The bug is that we are looking for the phrase lexical-binding
anywhere in the file. What we want is for it to exist in the first line of the file, and if it does not exist, we want to add it.
This bug meant I missed updating 3 files in my initial run.
I couldn't fix it using just ripgrep
, so this was what I replaced the first line with. If you know a better solution, please get in touch!
fd -e .el --exec sh -c 'head -n 1 {} | rg -qv lexical-binding && echo {}'
Here, I use fd
to find all emacs-lisp files, and then execute a small script on them. This script checks the first lines, and if the first line does not contain lexical-binding
, it outputs the filename!
Published On: Wed, 17 Apr 2024.