Search and replace over multiple files is difficult in my editor. There are plenty of tricks that can be done with find
, xargs
and sed
A UNIX way to do this with grep, xargs, and vi/vim:
This will help avoiding long commands when you want to exclude certain things from grep.
alias ge='grep --exclude=tags --exclude=TAGS --exclude-dir=.git --exclude-dir=build --exclude-dir=Build --exclude-dir=classes --exclude-dir=target--exclude-dir=Libraries --exclude=*.log --exclude=*~ --exclude=*.min.js -rOInE $*'
Command:
ge -l 'foo' .
Command:
ge -l 'foo' . | xargs -L 1 -o vim -c '%s/foo/bar/gc'
Explanation of the command:
The part till the pipe is the search command repeated
After the pipe, we call xargs to launch vim on each file
"-L 1" tells xargs to launch the command on each line instead of combining all lines in a single command. If you prefer to launch vim on all files at once, remove "-L 1" from xargs.
"-o" tells xargs to use the actual terminal rather than /dev/null
You have two options for this command:
a) Launch vim on all files at once
Advantages:
Disadvantages:
You have to switch to each buffer in turn and re-run the "%s" command on it
If you have a lot of files, this can make vim consume a lot of memory (sometimes as much as a full-blown IDE!)
b) Launch vim in turn for each file
Advantages:
Everything is automated. Vim is launched and performs the search command
Light on memory
Disadvantages:
I prefer variant b) as it's automated and more UNIX-like. With careful planification - inspecting the files list first, and maybe running the command on sub-directories to reduce scope - it is very nice to use.
General advantages:
It can be achieved with standard UNIX tools, available on virtually every platform
It's nice to use
General disadvantages:
You have to type the search expression twice
You might hit issues with bash escaping on complex search/replace expressions
I couldn't get Mario's answer to work, but if I use xargs it works.
Also, most times that I want to do search and replace in multiple files, I want to change an alphanumeric ID from one thing to another, such as renaming a variable or a class, etc. For that, it's very helpful to add a negative lookbehind and negative lookahead to disallow word characters before or after what you're replacing, and for that grep needs the P flag to use Perl regex:
vim `find . -name '*.cpp' -o -name '*.hpp' | xargs grep -Ple '(?<!\w)FIND(?!\w)'`
:bufdo %s/FIND/REPLACE/gce
Of course, it's helpful to wrap this up in a script so that you don't have to re-type it every time. Here's a start:
#!/bin/bash
if [[ $# < 2 ]]; then
echo "Usage: search-replace <search> <replace>"
exit 1
fi
search="$1"
replace="$2"
files=`find . -name '*.cpp' -o -name '*.hpp' | xargs grep -Ple "(?<!\w)$search(?!\w)"`
if [[ -z "$files" ]]; then
echo "No matching .cpp or .hpp files."
exit 1
fi
# The stuff surrounding $search is the negative lookahead/lookbehind
vim -c "set hidden | bufdo %s/\(\w\)\@<!$search\(\w\)\@!/$replace/gce" $files
Something that's still not working gracefully for me with Mario's solution is that at the end of the first file it says:
Error detected while processing command line:
E37: No write since last change (add ! to override)
This prevents the search and replace from flowing to the next file. It appears that "set hidden" fixes this, and so I've used that in my above script.
One remaining issue is that after doing search and replace on the first file, it seems to process that first file a second time, undoing your initial replaces. If anyone knows why that is happening, let me know.