Contents

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:

  1. We need the macro not to abort when runningf,on a line without a comma,

  2. We need the macro to do something different on those lines.

The solutions for both of these problems are related:

  1. We will add an extra comma so there is no “error”,

  2. 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

qq
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 normalf,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”ESCcharacter into the file: Vim has notated this as^[.

ReturnlhEsc

Next we add another line, enteringlhbefore 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 thef,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 thewmotion 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 thef,will move the cursor to the first comma on the line, thewwill move it to thesin second, and the+will move it down to thefat 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"0yank 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 bed2d,2ddor perhaps evendj. Here, instead, we useJDtwice. 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 performingddthe 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 doJDto 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 contain f"i=None^[ so the cursor will move to the next comma on the line, insert =None, and then leave insert mode (because the literalESCis 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"qregister 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!


  1. Okay, perhaps simple isn’t the right word, but I think it’s reasonably straightforward, and I haven’t remotely golfed it↩︎