HiveBrain v1.2.0
Get Started
← Back to all entries
patternbashMinor

Find-grep-sed for project-wide search & replace

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
searchsedgrepwidereplaceprojectforfind

Problem

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 & 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/[[::]]/${replacement}/g"
  fi
}


I'm on OSX, so unfortunately I had to use the ugly (and I assume non-portable) [[:<:]] word-boundaries for sed. I tried \b and \<, 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 ''`

Solution

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/[[::]]/${replacement}/g"


As for [[:<:]], 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.

Code Snippets

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" {} +
find . -name "$file_pattern" -print0 | xargs -0 grep -rwl "$search_pattern"
find . -name "${file_pattern}" -exec grep -Zrwl "${search_pattern}" \; |
xargs -0 sed -i '' "s/[[:<:]]${search_pattern}[[:>:]]/${replacement}/g"

Context

StackExchange Code Review Q#68244, answer score: 2

Revisions (0)

No revisions yet.