Bulk Editing in Vim

I regularly have to perform a short sequence of small, regular edits on a collection of files. If you work with computers long enough, that’s something everyone has to do.

Often I reach for a scripting language. But other times the edit is so small that even sed seems like overkill. Or maybe the edits are just the wrong kind of complexity to capture easily with code. My fingers may be able to make the changes quickly and repetitively, but when I try to break down how a script would do it, I get a headache.

Over the years, I’ve been confronted with problems like this often enough that I’ve developed a well-tested approach using Vim. It’s become one of those tools that I don’t really think much about: I just use it from time to time, and it makes my life easier.

But not long ago, Jeremy mentioned that he had a small change to make to a series of files in NeatlineMaps. Usually, he switches to TextMate for tasks like this, but he agreed that to let me show him how I would handle this in Vim.

Heh.

Whenever I try to explain to someone how to do something in Vim, I invariably sound like, Then hit escape, 4h, 0, now type whatever. It’s kind of funny, but it’s not a lot of fun, either for me or for the person I’m shouting keystrokes at.

Hopefully, this will make a better blog post.

Here’s what we did:

The Problem

Jeremy had tried to add some Vim mode lines to some PHP files. These are comments at the top of a file for setting options in Vim. Currently, they look like this:

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

But they weren’t working. It turned out that what should have been a colon near the end of the line was actually a semicolon, and once that was fixed, the settings worked fine.

That was all right. But he needed to make that change on almost every file in NeatlineMaps.

The Solution

The process I showed him has four parts. Let’s break them down.

Part One: :args

First, we have to load the files to process. When you open Vim from the command-line and pass in files there, the file names are stored in the argument list. You can access the argument list inside Vim—either to see what files are in it or to set the files it contains—using the :args command:

:args **/*.php

This searches for all the files in the NeatlineMaps directory and subdirectories that have a .php extension. These files are loaded into the argument list.

What’s nice about the argument list is how easily you can navigate over it using a few simple commands:

  • :rewind Moves to the beginning of the list.
  • :next Moves to the next file in the list.
  • :Next Moves to the previous file in the list.
  • :previous Also moves to the previous file in the list.
  • :last Moves to the last file in the list.

All these can also be abbreviated. So for example, you can use :n and :N to move forward and backward. Have a :n mapped to control-n, so navigating forward is especially easy.

Part Two: q

With the first file loaded into the buffer, now we make the change that we want to make on all files and record the keystrokes into a buffer. For this we chose the t buffer. There’s no reason for that particular letter: It was just the first one I thought of:

qt

Now the bottom of the Vim screen should say recording. At this point, we go ahead and make the edit.

Part Three: :s/../../e

What I had Jeremy do was slightly more complicated and precise, but basically, I had him do this:

:%s/softtabstop=4;/softtabstop=4:/e

This looks over the whole file (%) and performs a search-and-replace (s). It searches for the string softtabstop=4; and replaces it with the same string, except it used a colon (softtabstop=4:). The e at the end just means that it should ignore errors and keep chugging. That way, if a file does not have a modline (and not all did), it would keep going.

Once we’ve made the change, let’s save it and move to the next file.

:wn

This combines the write command and the next command (from above).

That’s all we need to do for each file. Now hit q to stop recording:

q

You can replay that now by pressing @t. Jeremy and I did that a few times to make sure it was doing what we wanted and wasn’t chewing up the files and spitting the pieces back in our faces.

Part Four: n@

Once you’re sure that everything’s safe, change the rest of the files. Most commands in Vim can take a numerical prefix, which tells Vim how many times to perform the command. For example, j moves down one line, and 10j moves down 10 lines.

In this case, tell it to play the recorded keystrokes 100 times:

100@t

And Vim goes to work. It will stop on the first error, which will happen when :next reached the last file in the argument list and isn’t able to move any further.

Solved

Well, looking back, this particular problem would have been perfect for sed. But sometimes that requires looking at documentation.

And that’s it. It seems more complicated than it actually is, and once you’ve been through it a few times, you can do it very quickly. Vim’s ability to record and replay keystrokes, combined with its commands to navigate in and across files, make an incredibly powerful combination.

To show how easy this process is, here is a screencast of me walking through the problem outlined above on NeatlineMaps code. You may want to click through to a larger version, more readable version of the video.

Bulk Editing Vim from Eric Rochester on Vimeo.

My interests include text processing, text mining, and natural language processing, as well as web-development and general programming. Studied medieval English literature and linguistics at UGA. Dissertated on lexicography. Now I program in Haskell and write when I'm not building cool stuff for the Scholars' Lab. Also, husband and parent. Do you notice that sleep…

5 Comments

  1. Nice solution.
    Macros can be called recursively so when recording your macro you could have finished with “@tq”. Then, when you call “@t” it will edit the first file, move to the next and then call itself so you don’t need to use “100@t”, just “@t”.

  2. There is a bug in “Part Three” this code:

    :%s/softtabstop=4;/softtabstop=4:/e

    should not have the “span” tags:

    :%s/softtabstop=4;/softtabstop=4:/e

  3. Thanks for this post!

    I was about to go with your solution when I ran across a pointer to the :argdo command built into vim. Though it might be useful to you as well.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Archives