Friday, July 01, 2011

Conditionally applying macro recordings.

I make and use recordings in Vim all the time. I cannot imagine my daily work-a-day life without them. The most common reason for using recordings is to perform a complex modification of a semi-formatted dataset: SQL result sets, CSV, TSV, XML, HTML, etc...

As with most things in Vim when you find some new way feature, you find that you probably already had the parts laying before you, but hadn't quite had the 'manual' to put all the parts together. Recently I found a new way to do so conditionally, that I thought I'd share; its one of those methods that has probably been staring me in the face for-ever, and I just didn't see it.

So. What am I talking about? Conditional Macros. Lets break it down:

Macros

Plain and simple. Here is a simple problem that I think requires a macro. Suppose you have some lines of text like the following:

List A: 108 unique colors, 387 total colors
List B: 266 unique colors, 1343 total colors
List C: 361 unique colors, 2554 total colors
List D: 174 unique colors, 1221 total colors
List E: 301 unique colors, 2665 total colors

Suppose you wanted to compute the difference between the total colors and unique colors. So, in the case of the first line this would be 387 minus 108. You can do this once, sure. But suppose there are a couple thousand lines, and you want it all done, and done in Vim. No problem. You would record a macro like so:
  1. start your macro recording to register m (qm)
  2. go to the beginning of the line (^)
  3. move up to the first number (WW)
  4. store the first number in a register ("iyw)
  5. go to the second number (f, )
  6. store the second number in a register("oyw)
  7. compute the difference between the numbers (:let @s=@o-@i[carriage return])
  8. print out the result at the end of the line ($a == [control r]s[control c])
  9. move to the next line (j)
  10. stop recording (q)
If you were to examine register m, you would see:

^WW"iywf, "oyw:let @s=@o-@i[carriage return]$a == [control r]s[control c]j

And if you executed this macro (@m) for each line you would get something like this:

List A: 108 unique colors, 387 total colors == 279
List B: 266 unique colors, 1343 total colors == 1077
List C: 361 unique colors, 2554 total colors == 2193
List D: 174 unique colors, 1221 total colors == 1047
List E: 301 unique colors, 2665 total colors == 2364

To do several lines you would replay the macro by typing something like 5@m to do it five times, etc.

Conditional

When conditional modification pops to mind, I think of the global command (:g//). Whenever there is a match for the global command, it executes some arbitrary commds. So suppose you had text like the following:

...
...
Uninteresting line
List A: 108 unique colors, 387 total colors
Uninteresting line
Uninteresting line
List B: 266 unique colors, 1343 total colors
Uninteresting line
List C: 361 unique colors, 2554 total colors
Uninteresting line
Uninteresting line
...
...

You could just delete all the uninteresting lines like so:

:g/^Uninteresting line$/delete

Conditional Macros

Now, you could just delete everything not interesting and then run your macro on whatever is left. But a lot of times, you might find that you are interested in 'fixing' some lines but leaving the rest of the text unchanged. You can do this by combining the global command with a macro. So suppose you had:

...
...
Mildly interesting line
List A: 108 unique colors, 387 total colors
Mildly interesting line
Mildly interesting line
List B: 266 unique colors, 1343 total colors
...
...

To execute the macro you created, but only for the interesting lines you can combine the macro with the global command:

:g/^List/norm @m

Which would yield the desired result:

...
...
Mildly interesting line
List A: 108 unique colors, 387 total colors == 279
Mildly interesting line
Mildly interesting line
List B: 266 unique colors, 1343 total colors == 1077
...
...

2 comments:

Kris Malfettone said...

You also could do the search before you start recording; then at the end of the macro before you stop recording just make sure you press n to take you to the next match.

The only thing less efficient is you somewhat need to know how many lines you are going to replace or just hold down @ after you run it once :)

But I really like the idea of the global command. Thanks for the post.

Unknown said...

Right right! Often when I record something I do just as you suggest. In most cases, especially really regularly formatted data (not so much mostly uninteresting text), I think thats the way to go.

Since I 'discovered' tying global to record I think I've figured out that its useful for more complicated recordings. Often when I cook up some complicated recording I have to re-record b/c I mess it up in some little way. Its nice to completely take the 'next search' or 'move down to the next unprocessed section' out of the recording and just focus on the task at hand...which is what the global command lets you do.

Thanks for the comment!