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:

  1. Some files already contain this directive, so they shouldn't be modified
  2. Some files do not start with ;;;, these should be ignored.

With those caveats in place, here is the tiny shell puzzle:

  1. Find all the files which need to be modified.
  2. 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 contain lexical-binding, and print only filenames.
  • Line 2: Pipe the filenames to xargs for pushing one-by-one to awk.
  • 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.