Changing GNU Emacs Lisp functions through advice-add
, not brute force
It's a tradition with me that sooner or later, I hit a GNU Emacs
function that doesn't work the way I want it to and has no applicable
customization options. My traditional brute force approach to dealing
with these functions has been to redefine them; I'd copy their code
to my .emacs or some personal .el file, modify or replace it to
taste, and then insure that my definition got used instead of the
standard one. If what I really cared about was a keybinding, sometimes
I could give my version a new name (and bind the key I cared about
to it). Recently, however, Ben Zanin pointed me at advice-add
and, after a
while, I was able to work out how to do some changes with it in a
nicer way than my previous brute force approach. So here are some
notes.
The simplest situation for advice-add
is when you want to completely replace a function with a different
implementation, which is easily done with ':override
':
(advice-add 'mh-forwarded-letter-subject :override (lambda (_ignored subject) (concat "Fwd: " subject)))
This redefines the function that generates the Subject: header for
email forwarded in MH-E
so that it completely ignores the author of the mail being forwarded
(this turns out to be possible through standard customizations
because format
can use (only) specific arguments, unlike C printf formatting, which
I didn't know at the time I hacked this up).
Sometimes I want a function to not do something, for example to
never use NMH's anno
to
modify my existing email messages. MH-E
doesn't directly expose this as a customization, but it does have
a specific function to execute (N)MH commands and we can just not
run that function if we're trying to execute anno
:
(advice-add 'mh-exec-cmd :before-until (lambda (cmd &rest args) (string-equal cmd "anno")))
The Ways to compose advice
section is a little bit confusing, but my rule of thumb is that
':before-until
' is for situations where I don't want the function
to run if something is true, while ':before-while
' is for situations
where I only want the function to run if something is true.
The brute force version of this is to use ':around
', which lets
you wrap around the original function in a single function of your
own:
(defun no-annotate (oldfun cmd &rest args) (if (not (string-equal cmd "anno")) (apply oldfun cmd args))) (advice-add 'mh-exec-cmd :around 'no-annotate)
Another thing you might want to do is modify the argument list of
a function, for example to change how MH-E handles email replies
so that you always automatically get cc'd on them. This is done
with ':filter-args
' advice, but this advice is slightly tricky
because as far as I can see you don't get passed exactly the normal
arguments; instead you just get a forced list of them. So you have
to write your modification like this:
(defun repl-cc-me (args) (if (string-equal (car args) "repl") (append args '("-cc" "me")) args)) (advice-add 'mh-exec-cmd :filter-args 'repl-cc-me)
Although it may be an obvious thing to say, if you're going to
filter the function's arguments you're going to have to know exactly
what arguments it's passed, and there may be surprises. For example,
mh-exec-cmd
turns out not to always be passed a list of just
strings; sometimes there may be numbers or sublists mixed in.
You can advise the same function (such as mh-exec-cmd
) multiple
times to do different things. Here I've advised it twice, one to
ignore attempts to run "anno" and once to modify what "repl" gets
handed as an argument. Obviously you need your advice to not clash
(as is the case here); otherwise you may need to combine them in a
single piece of advice that sorts it out right.
As both of these examples illustrate, you may need to advice-add
some other function than your initial target. My initial targets
are the behavior of 'mh-forward
' and 'mh-reply
', but in both
cases the behavior is in the middle of the function, so unless I
want to re-define them I have to hook into something else. Finding
where to hook into requires reading the code and following control
flow. If you need to alter one function's use of another function
that is generally used (such as 'mh-exec-cmd
', which is used all
over MH-E), you generally need to hope that it's called with some
argument you can detect that's specific to your function.
(In fact 'mh-forward
's annotation behavior is a couple of layers
of functions down, but once I found the root cause I decided I
didn't want any annotation to be happening from anywhere, not just
forwarding messages.)
One somewhat tricky option here that I didn't actually get working the one time I tried it is to create a dynamically scoped variable that you use to signal that you are in your function of interest:
(defvar mh-reply-marker nil "Are we in mh-reply?") (defun mark-mh-reply (oldfun message &optional reply-to includep) (let ((mh-reply-marker t)) (apply oldfun message reply-to includep))) (advice-add 'mh-reply :around 'mark-mh-reply)
Then your advising code for other functions can check mh-reply-marker
to see if they should do things or just quietly stay out of the
way. This is a real usage case for ':around
', because we need
to wrap the original function in that 'let
'.
The other thing that I haven't gotten fully and completely working
is advising interactive functions. In theory this is supposed to
work, but in practice at various times either invoking the function
itself has failed or other functions that do '(call-interactive
'mh-reply)
' have failed with mysterious errors. My conclusion is
that this is a level of advising that is currently beyond my ken,
and if I need to dabble in the waters to this depth, I'm probably
back to re-defining functions (it may be brute force but it works).
Sidebar: 'let
' and dynamically scoped variables
If you're new to Lisp and dynamic scoping, it may be a little bit
surprising that your in-function 'let
' of
'mh-reply-marker
' persists even into functions that you call, but
it does and this feature is actually used all over GNU Emacs in
various ways. One example is temporarily forcing the value of a
customizable setting to do something. MH-E can be set to prefer
plain text over HTML for email with both, and it has a function
'mh-show-preferred-alternative
' to override that temporarily;
this function works by nulling out the customization you set and
re-displaying the message. You can write an inverse of this with
the same idea:
(defun mh-show-plaintext () "Show text/plain instead of text/html for a message." (interactive) (let ((mm-discouraged-alternatives '("text/html"))) (mh-show nil t)))
(I have other personal functions that work this way.)
|
|