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.