a bit of emacs advice

A couple months back, a friend posted about some difficulty installing an Emacs package, which turned out to be due to out of date package metadata. He solved his problem with a comment in his config (read his post!), but I still tooted at him about how I’d solved the same problem in my config by advising a function — “advice” being a way that Emacs allows you to automatically run some code of your own before or after you run a given function (or even around a given function, if you want to get extra fancy).

I linked to the section in my config where I’d done this, and made a mental note to write a blog post explaining the advice system a bit, intending to use a different place I’d added some advice more recently as the basis of my post. When I finally sat down and started drafting this post, I ended up looking at the Emacs advice documentation for the first time in …maybe ever? and I discovered that I’d been writing advice the old way, not the current way, which led to a bunch of cleanup in my config. (Yaks shaved while you wait!)

I’m gonna focus just on the cleanup of the function Brian and I were talking about.

So, here’s the problem we’re trying to solve: when we call the package-install function, if we’ve haven’t downloaded fresh package metadata during our current Emacs session, we want to call the package-refresh-contents function before the package-install function runs.

One approach to solving this problem would be to write our own version of package-install, wrapping the existing function, and handle calling package-refresh-contents as needed — but other parts of Emacs that call package-install wouldn’t know about our new function, and we still might run into the stale package metadata problem if we invoke package-install indirectly via one of those other functions.

There are various shenanigans we could play to save the old function under a different name, and then put our own replacement function in its place — but that’s sort of ugly and honestly a bit of a pain in the ass, and the whole point of this post is about the advice system, so you probably know we’re gonna use that. So, onward.

My previous solution — which I probably borrowed and adapted from somebody else, long ago — looked like this:

(defvar genehack/packages-refreshed nil
"Flag for whether package lists have been refreshed yet.")

(defadvice package-install (before refresh activate)
"Call `package-refresh-contents` once before `package-install`."
(unless (eq genehack/packages-refreshed t)
(progn
(package-refresh-contents)
(setq genehack/packages-refreshed t))))

So, we declare (via defvar) a flag variable to track whether or not we’ve downloaded packages during this session or not, defaulting it to nil. Then we call the defadvice macro to handle advising the package-install function — we tell it we want to run this code before the advised function, we provide the token refresh as an identifier for our code, and we tell Emacs to activate the advice.

The bit that starts with (unless... is the part that does the work. If the genehack/packages-refreshed flag is anything other than t, we call package-refresh-contents and then we set the flag to t, so the next time this happens in this Emacs session, we won’t download the package metadata.

Make sense? Sorta? Now, there’s a lot of …magic crap in here. I’m sure the first few times I used this form, I probably cargo-cult-ed the various bits, and unless you get all of them in the right place (particularly the activate bit), the advice won’t work. So I can understand the desire to replace it with something better.

Here’s the new-and-improved version:

(defvar genehack/packages-refreshed nil
"Flag for whether package lists have been refreshed yet.")

(defun genehack/package-refresh (&rest args)
"Refresh package metadata, if needed.
Ignores `ARGS'."

(unless (eq genehack/packages-refreshed t)
(progn
(package-refresh-contents)
(setq genehack/packages-refreshed t))))

(advice-add 'package-install :before #'genehack/package-refresh)

We start out with the same flag variable. But now we’re using defun to declare a regular Emacs function. This function is going to get called with the same arguments as the advised function, but we’re not going to use any of them — but we still need to have the function accept arguments, or it’ll throw an error when called. The (&rest args) bit takes care of that by slurping up whatever arguments we’re given. (We note in the docstring that we don’t use the args, because otherwise the Emacs Lisp linter will yell at us.)

The core of this function is the same as from the previous advice, so I won’t go over it again.

The other different bit is at the end: it uses the advice-add function to apply the advice to the package-install function, using the :before symbol to indicate the position of the advice, and then provides the advising function as a function reference (i.e., #'genehack/package-refresh).

Six of one, half-dozen of the other — both versions do the exact same thing; the second one is just the one currently recommended by the documentation — and it is maybe a little bit easier to understand how the different bits get fitted together when they’re broken down to start with and put together in the end by advice-add.

Now that I’ve laid some groundwork, next time, perhaps, I’ll talk about the advice example I originally intended to talk about, which is slightly more complex, and slightly more interesting.