Hack and /

Return of the Mac

Kyle Rankin

Issue #246, October 2014

Save yourself keystrokes by building a library of vim macros to automate mundane text-editing tasks.

In my July 2014 column, I talked about vim macro basics. In that article, I described how to record a custom macro, assign it to a key and then use it to make automated edits to a BIND zone. I also teased that I would cover more advanced uses of macros, like nested macros, in a future issue. I took a brief detour to cover a few different topics, but now I'm back on topic, and in this article, I discuss more complicated uses for macros.

I like using BIND zone files for macro examples, because it's the file I most often use macros in myself. Because multiple people often edit zone files, they may not all have the same formatting. Plus, the top of a zone file generally has a different multi-line format compared to the rest of the file. In my July 2014 article, I talked about how to add 50 sequential A records in a zone file using a single macro, but when I have to perform more complicated edits, or if I have to perform edits selectively in some files but not others, I've found it useful to record a few different simple macros under different keys, then record a new macro that just calls those other macros in a particular order. Among other things, this lets me change some of the shorter macros if I need to, without having to re-record everything.

For the first example, let's look at a more complete version of the BIND zone file I used last time:

;
; BIND data file for example.com
;
$TTL 15m
@       IN      SOA     example.com. root.example.com. (
                        2014081500
                        10800
                        1200
                        7200
                        7200 )
;
example.com.   IN      NS      ns1.example.com.
example.com.   IN      NS      ns2.example.com.
;
ns1      IN A  10.9.0.5
ns2      IN A  10.9.0.6
;
worker1  IN A  10.9.0.15
worker2  IN A  10.9.0.16
worker3  IN A  10.9.0.17
. . .
worker50 IN A  10.9.0.64

In this example, let's say I have 15 zone files for different zones, but I want to make the same set of edits to all of them. I want to change the TTL in the file to be 10 minutes, and I need to change the contact info for the domain from root@domain to dnsadmin@domain. I also need to increment the serial number (2014081500) in the zone file, and I need to change the name server IPs all to point to a new set of name servers at 10.1.0.250 and 10.1.0.251, and finally, I want to add 50 more workers to each zone file.

Although I imagine I could build all of this into a single big macro, for me, it makes sense to split up the steps into at least four macros that I already have pre-assigned letters to:

  • Macro t will change the TTL.

  • Macro s will change the contact information and increment the serial.

  • Macro n will update the name server records.

  • Macro w will add one more worker.

Because I'm going to chain these macros together, it's even more important than in the past that I make sure my cursor is anchored in a known location first. For the first macro, this means starting with gg to move to the very top of the file, while for the last macro, I will want to type G to move to the bottom of the file. At each phase, it's incredibly important that you test each macro, then undo the changes and confirm that your macros work exactly how you expect. Let's break down each macro.

For macro t, I first type qt to enter macro mode and assign the macro to the t key. Then I type gg to make sure I'm at the top of the file. Next I type /TTL <Enter> to move the cursor to the TTL line. Then I type w to move forward a word to the actual TTL value I want to change, and then I type cw10m to change the following word from 15m to 10m. Finally, I press Esc to exit insert mode, and q to exit the macro. The complete set of macro keystrokes then would be qtgg/TTL <Enter> wcw10m <Esc> q. Once I record the macro, I type u to undo my changes, and then test the macro by typing @t.

For macro s, I type qs to enter macro mode and assign the macro to the s key. Then I type gg again to anchor to the top of the file. Next I type /SOA <Enter> to move to the SOA line. Then I type /root <Enter> to move to the beginning of root.example.com, and then type cwdnsadmin <Esc> to change that word to dnsadmin and exit insert mode. Next I type ^j to move the cursor to the beginning of the following line. Finally, I type w <Ctrl-a> to move forward to the serial number and increment it, and then q to exit macro mode. The complete set of macro keystrokes becomes qsgg/SOA <Enter> /root <Enter> cwdnsadmin <Esc> ^jw <Ctrl-a> q. And again, I save the macro and use u to undo the change and replay it with @s to make sure it does what I expect.

For macro n, I type qn to enter macro mode and assign the macro to the n key. Then I type gg to anchor to the top of the file. Next I type /^ns1 <Enter> to move to the line that configures the name servers. At this point, there are a few ways I could edit these lines. My preference is to type /IN A <Enter> 2w, which will move my cursor to the beginning of the IP. Then I type c$10.1.0.250 <Esc> to edit to the end of the line and exit insert mode.

Since ns2 is so similar to ns1, I can just type yyp <Ctrl-a> $ <Ctrl-a> to copy and paste ns1, increment ns1 to be ns2, then move to the end of the line and increment the IP. Next I need to find the existing ns2 line and delete it with /^ns2 <Enter> dd. Finally, I can type q to save the macro. The complete macro is qngg/^ns1 <Enter> /IN A <Enter> 2wc$10.1.0.250 <Esc> yyp <Ctrl-a> $ <Ctrl-a> /^ns2 <Enter> ddq. Although this seems like a lot of text, it will save a ton of work when you have to repeat the steps on multiple files.

For the final macro w, I type qw to enter macro mode assigned to the w key, and then type G to move to the bottom of the file this time. Then I type /^worker <Enter> N to search for the next worker (which will wrap around to the top, then N will move back to the last worker in the file). Finally, I type yyp to copy and paste the line, then <Ctrl-a> $ <Ctrl-a> to increment both the worker hostname and the IP. Finally, I type q to exit macro mode. The complete macro is then qwG/^worker <Enter> Nyyp <Ctrl-a> $ <Ctrl-a> q. Like the others, I test it out with @w a couple times, and use u to undo all the changes in between until I am satisfied that it works.

Now that I have all of these macros recorded, I could just open each file and type @t to update the TTL, @s to edit the contact information and serial, @n to update the name servers, and type 50@w to add 50 more workers.

Because I'm going to perform these same steps on a number of files, I might as well capture all those commands in a new macro I'll assign to c. To do that, I just type qc to enter macro mode assigned to the c key, then type @t@s@n50@w to perform all of the previously recorded macros. Finally, I type q to exit macro mode. The complete set of keystrokes is qc@t@s@n50@wq to assign all of the above sets of keystrokes to a single macro. Now when I open the next file, I can just type @c to perform the complete list of steps.

Now besides saving a few keystrokes, there are other good reasons to nest macros in this way. Because I saved each logical step as its own macro, I can tweak or modify any of the above macros independently, save the new macro to the same key, and all of the other macros, including the final combo macro can stay the same.

Let's say that after I recorded all of these macros, I realized I got the IP address for the name servers wrong. All I would have to do is record a new macro assigned to the same n key, and once I was done, I still could use @c to make the complete set of changes to a file.

I hope you find these examples useful, and that the next time you have to perform a series of mundane edits to many text files, you'll save some keystrokes with vim macros.

Kyle Rankin is a Sr. Systems Administrator in the San Francisco Bay Area and the author of a number of books, including The Official Ubuntu Server Book, Knoppix Hacks and Ubuntu Hacks. He is currently the president of the North Bay Linux Users' Group.