Find-grep-sed for project-wide search & replace I always forget how to do this efficiently with Vim's arglist. Drawing inspiration from a post over at Stack Overflow, I wrote a Bash function to perform a project-wide search &amp; replace.

It does the following:


find files matching a file-pattern
narrow down to those files containing a search-pattern
within those files, replace all occurrences of the search-pattern


Example usage: greplace **.rb old_method new_method

greplace() {
  if [ "${#}" != 3 ]; then
    echo "Usage: greplace file_pattern search_pattern replacement"
    return 1
  else
    file_pattern=$1
    search_pattern=$2
    replacement=$3

    find . -name "${file_pattern}" |
    xargs grep -rwl "${search_pattern}" |
    xargs sed -i '' "s/[[:&lt;:]]${search_pattern}[[:&gt;:]]/${replacement}/g"
  fi
}


I'm on OSX, so unfortunately I had to use the ugly (and I assume non-portable) [[:&lt;:]] word-boundaries for sed. I tried \b and \&lt;, with and without -E (for extended regular expressions), but still no dice.

I also debated whether to bake the word-boundaries in or leave them up to the caller (my poor little fingers). But I can't imagine wanting use this without word boundaries. I'd much rather miss a replacement because it's a variation on the search term than accidentally mangle a word happens to contain the search term.

Another potential issue is the fact that I'm using sed -i '' to perform the in-place replacements without making any backup files. This seems fine for my use, since I'm invariably using Git for version control. I'm curious how big a risk this is though.

Update Props to janos for his pointers and improvements. Here's an updated version:

greplace() {
  if [ "$#" != 3 ]; then
    echo "Usage: greplace file_pattern search_pattern replacement"
    return 1
  else
    file_pattern=$1
    search_pattern=$2
    replacement=$3

    # This works with BSD grep and the sed bundled with OS X.
    # GNU grep takes `-Z` instead of `--null`.
    # Other versions of sed may not support the `-i ''` syntax.

    find . -name "$file_pattern" -exec grep -lw --null "$search_pattern" {} + |
    xargs -0 sed -i '' "s/[[:&lt;:]]$search_pattern[[:&gt;:]]/$replacement/g"
  fi
}
Not bad so far!

You seem to like ${name} instead of $name. That's fine, but it's not needed anywhere in the posted code. Usually it's needed in situations like ${name}_suffix, where without the ${...} the "_suffix" would become part of the variable name.

In general, the best way to find files and do something to them is using the -exec flag of find, for example:

find . -name "$file_pattern" -exec grep -rwl "$search_pattern" {} \;
# or end with + to run on a single line
#find . -name "$file_pattern" -exec grep -rwl "$search_pattern" {} +


The second best way is to pipe to xargs -0, and use -print0, for example:

find . -name "$file_pattern" -print0 | xargs -0 grep -rwl "$search_pattern"


Your case has an added twist: you also want to pipe the matched filenames from grep, and like find, the proper way is to use NUL character as the filename separator. In GNU grep the flag to achieve this is -Z. Putting the above together, the safest way to write your chained command:

find . -name "${file_pattern}" -exec grep -Zrwl "${search_pattern}" \; |
xargs -0 sed -i '' "s/[[:&lt;:]]${search_pattern}[[:&gt;:]]/${replacement}/g"


As for [[:&lt;:]], I don't know another way on OSX. In any case, the sed -i '' filaname usage won't work with GNU sed, so this script is not portable.
It would be good to add comments near the top of the file to highlight this fact to whoever will inherit this script from you at some point in the future.
(Uhm, or in any case, for the "future you" ;-)

Lastly, I'm wondering if the -r flag for grep is intentional.
It's for recursive search in sub-directories, not only files.
If that's not intended, then drop that flag.