Whenever I encounter a problem that’s painful to solve, I consider finding a better way to do it. This involves weighing how long I think it might take to find (or create) a better solution versus the amount of time it would take to suck it up and do it the hard way. Where this fails is with painful processes encountered infrequently; since they don’t happen often, it’s less worthwhile to invest the time to improve it.
Multi-file search and replace is a perfect example of this kind of infrequently painful process. After messing around with for loops and sed in the shell, I finally sucked it up and got familiar with the Emacs way of doing it. It’s a great example of the synergy of Emacs tools, since it combines familiar features in a new way.
The facility for one-file S&R is called query-replace, and it’s bound to M-% (plain strings) and C-M-% (regexes) by default. As you’d expect, it prompts for search and replacement text, then replaces it in your buffer. When using regular expressions, you have the full power of them, with the ability to capture sub-expressions and use them in your replacement.
Dired is the Emacs file management package, and it’s quite powerful. You can invoke it with C-x d, or by pointing find-file (C-x C-f) at a directory.
Dired lets you mark files with m, or files matching a regular expression with % m. Pressing Q runs query-replace over the contents of those files.
Dired only shows files in one directory, so this doesn’t work if you want to replace in files which span directories.
This can be mostly solved with find-dired. As the name implies, it runs find, putting the results in a dired buffer. Once you have that, you can use Q to S&R in the matched files.
This is good if you need to replace a string in every file under a subdirectory, or in every file whose name matches a pattern. Where it fails is when you need to replace a string in files which contain that string.
Emacs has excellent integration with grep, in several flavors:
grep. Basic grepping, with results placed in *grep*. The buffer shows filenames, line numbers, and the content from the file which matched.rgrep. My favorite, rgrep performs a recursive grep and filters out unwanted files, such as backups, stuff in .svn, etc.lgrep. Just like rgrep, but not recursive.find-grep. Getting close to what we want, find-grep runs grep on files located with find. And finally…find-grep-dired. Just like find-grep, but puts the results in a dired buffer instead.As you’ve no doubt figured out, we’ll be using find-grep-dired for this task.
Now we have all the pieces, let’s put them together. As an example, let’s say that we renamed an exception and need to change the catch blocks in every .php file in package.
We start by running find-grep-dired:
M-x find-grep-dired RET catch (FooException RET
This gives us a buffer of files containing catch (FooException. It includes stuff in .svn; we only want .php files, so we can mark those:
% m .php$ RET
Now that they’re marked, run query-replace on them:
Q catch (FooException RET catch (BarException RET
At this point, Emacs will cycle through every match in every file and ask you to confirm the replacement. If you press !, it will replace the rest of the matches in the current file, and start prompting you for the next; Y will replace every match in every file with no further prompting. Then you’ll need to save the files:
C-x s
This saves every modified file at once, and we’re done.
Discussion