Macro Conditional
The Question
After seeing the macro Game of Life, Benoit de Chezelles asked on Mastodon:
I replied:
But just because something isn’t practical, doesn’t mean it’s not possible, and so I decided to write this post to explain how you can write a macro that acts differently depending on whether anf,
motion succeeds or fails.
The Challenge
Benoit politely declined to give me a real-life scenario where he needed this, so I whipped up the following CONTRIVED editing task.
Record a macro that, when run on each line, turns this:
{"name": "func_a", "parameters": "param"}
{"name": "func_b", "parameters": "first, second"}
{"name": "func_c", "parameters": "param_1, param_2"}
{"name": "func_d", "parameters": "single_param"}
Into this:
def func_a(param):
def func_b(first, second=None):
def func_b(param_1, param_2=None):
def func_d(single_param):
This is, I admit, RIDICULOUS. Why isn’t that quite JSON? Why don’t the Python functions have bodies? Why wouldn’t you just do this with a :substitute
command?
BUT! It demonstrates the technique — how to get the macro only to add the =None
default parameter value ONLY when there is a comma in the parameter list — and doesn’t include too many other, unrelated edits.
What can I say, I’m good at macros, not coming up with examples.
The Technique
There are two issues we need to deal with:
-
We need the macro not to abort when running
f,
on a line without a comma, -
We need the macro to do something different on those lines.
The solutions for both of these problems are related:
-
We will add an extra comma so there is no “error”,
-
We’ll make use of the fact that the cursor will move to a different location on the line depending to which comma it moves.
The Solution
To edit the “before” into the “after”, type the following:
qqc3f"defSpaceEsc;c3;(EscASpace,Escof"i=NoneCtrl-VEscReturnlhEsc--f,w+y$?^defReturnJDJD0@0f"C):Esc+q3@q
As always, written out as a series of keystrokes like this it can look a little INSCRUTABLE. But it’s much simpler than it appears!1 Let’s break it down:
How It Works
- First, we start a macro recording.
- c3f"defSpaceEsc
-
Then, we change as far as the third quote
"
on the line, entering the text:def
. - ;c3;(Esc
-
Next, we move to the next quote and again change as far as the third quote, this time entering a single
(
bracket. So far, this is all standard Vim editing stuff.At this point, the line contains the following text:
def func_a(param"}
So this is where we need to add our conditional. Now, if we were to perform a normal
f,
at this point, the macro would abort as there is no comma on the line. DISASTROUS! So first let’s fix that: - ASpace,Esc
-
We append a space and a comma to the end of the line:
def func_a(param"} ,
- of"i=NoneCtrl-VEsc
-
Next, we add a new line below, and enter some contents:
def func_a(param"} , f"i=None^[
I’m going to hold off explaining why we did this for a moment. For the time being, I’ll just point out that we are still in insert mode. Because we typed Ctrl-V before hitting Esc, instead of leaving insert mode we have instead entered a “literal”
ESC
character into the file: Vim has notated this as^[
. - ReturnlhEsc
-
Next we add another line, entering
lh
before we leave insert mode:def func_a(param"} , f"i=None^[ lh
Don’t worry if you don’t know understand why we did this. All will become clear shortly!
- --
- Then we move up two lines.
- f,
-
Now, finally, we can do the
f,
motion we wanted to do four steps ago! - w+
-
And now we get to do the conditional! We move forward a word, and then to the start of the next line. But WHY? Well, when we do this on our current line, the cursor is on the comma at the end of the line, so the
w
motion will wrap us onto the second line, and then the+
will move us down onto the third line so the cursor is now on thel
.But when we get to this point while running the macro on the next line to be edited, our buffer will look like this:
def func_b(first, second"} , f"i=None^[ lh
So the
f,
will move the cursor to the first comma on the line, thew
will move it to thes
insecond
, and the+
will move it down to thef
at the start of the second line.The cursor is on a different line depending on whether a comma was (originally) present.
Condizionale!
- y$
-
Now, we yank the contents of whatever line we are on. Vim will store what we yanked in the
"0
yank register. - ?^defReturn
-
Back into normal motions/editing for a bit. We perform a backwards search to move back to the
def
regardless of which line we’re on. - JDJD
-
The normal way to delete two lines would be
d2d
,2dd
or perhaps evendj
. Here, instead, we useJD
twice. The only reason for this CONTORTION is to ensure we end up in the same location even if we are at the very bottom of the buffer.Regular linewise deletion doesn’t work in this way. Normally, after performing
dd
the cursor ends up on the line below the one that was deleted. But when the deleted line was the last line in the buffer, the cursor ends up on the line above. This INCONSISTENCY would break a macro. So instead we start with the cursor on the line above, and then doJD
to delete the line below the current one, without changing the vertical location of the cursor. - 0
- Now we move to the start of the line and…
- @0
-
Run the macro in the yank register
"0
. When we are recording the macro, the yank register containslh
, so we move left and then immediately right again. Nothing happens. But when we run the macro on the next line, the yank register will instead containf"i=None^[
so the cursor will move to the next comma on the line, insert=None
, and then leave insert mode (because the literalESC
is replayed too).Look, Ma, I did a conditional!
- f"C):Esc
-
We finish off by moving to the last quote on the line and changing from there to the end of the line to add the final frowny face
):
. - +q
- All our edits are done. We move to the start of the next line and stop recording. Our conditional macro is complete.
- 3@q
- All our hard work paid off! We can now run our macro three times to edit the remaining three lines. Satisfying!
If you’d like to try this macro out, but think it looks like too much typing, here’s a command you can paste into Vim’s command-line to set up the"q
register directly.
:let @q="qqc3f\"def\<Space>\<Esc>;c3;(\<Esc>A\<Space>,\<Esc>of\"i=None\<C-V>\<Esc>\<CR>lh\<Esc>--f,w+y$?^def\<CR>JDJD0@0f\"C):\<Esc>+q3@q"
The Conclusion
I have banged this drum before, and I will continue banging it until everyone within earshot has boarded the macro-train: because of the sheer expressiveness of Vim’s normal mode commands, Vim macros are an incredibly powerful tool. I hope the above has helped INSPIRE anyone that doesn’t already use them to hop on board. Choo choo!
If you liked this post, why not go checkout some of my other posts on macros. Or better yet, subscribe! One day I might even write a post that isn’t about macros. Preposterous!