4
\$\begingroup\$

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/[[:<:]]${search_pattern}[[:>:]]/${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 ''` syntax.

    find . -name "$file_pattern" -exec grep -lw --null "$search_pattern" {} + |
    xargs -0 sed -i '' "s/[[:<:]]$search_pattern[[:>:]]/$replacement/g"
  fi
}
\$\endgroup\$
2
\$\begingroup\$

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/[[:<:]]${search_pattern}[[:>:]]/${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.

\$\endgroup\$
  • \$\begingroup\$ Great answer, thanks! I was starting to wonder about that -r flag as well. I'm curious why the NUL byte is the preferred separator. Also confused about the difference between using \; and + to terminate the exec line. Both yield the same output, but I guess they're performing differently internally? \$\endgroup\$ – ivan Oct 29 '14 at 13:18
  • \$\begingroup\$ It doesn't seem to like using grep -Z and xargs -0. I'll keep playing around with it, see what I can glean. \$\endgroup\$ – ivan Oct 29 '14 at 13:38
  • \$\begingroup\$ The purpose of using NUL as the record separator is to protect from special characters, for example a newline in filenames. This might be a somewhat paranoid practice, but this still the recommended approach. grep -Z and xargs -0 work fine for me. The difference between \; and + is that with \; it will run the exec command for each argument, and with + it will run one command with many arguments. If you call it "great answer" then perhaps an upvote is due :P \$\endgroup\$ – janos Oct 29 '14 at 14:31
  • 1
    \$\begingroup\$ My bad, I was hurrying out the door and forgot to accept and upvote. Also figured out my problem with -Z. The BSD grep on OS X takes --null instead (and uses -Z to handle compressed files). Thanks again for the pointers, I learned a lot from this. \$\endgroup\$ – ivan Oct 29 '14 at 16:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.