Command line: search and replace in all filenames matched by grep

I'm trying to search and replace a string in all files matched by grep:

grep -n 'foo' * will give me output in the form:

[filename]:[line number]:[text]

For each file returned by grep, I'd like to modify the file by replacing foo with bar.

105091 次浏览

Do you mean search and replace a string in all files matched by grep?

perl -p -i -e 's/oldstring/newstring/g' `grep -ril searchpattern *`

Edit

Since this seems to be a fairly popular question thought I'd update.

Nowadays I mostly use ack-grep as it's more user-friendly. So the above command would be:

perl -p -i -e 's/old/new/g' `ack -l searchpattern`

To handle whitespace in file names you can run:

ack --print0 -l searchpattern | xargs -0 perl -p -i -e 's/old/new/g'

you can do more with ack-grep. Say you want to restrict the search to HTML files only:

ack --print0 --html -l searchpattern | xargs -0 perl -p -i -e 's/old/new/g'

And if white space is not an issue it's even shorter:

perl -p -i -e 's/old/new/g' `ack -l --html searchpattern`
perl -p -i -e 's/old/new/g' `ack -f --html` # will match all html files

If your sed(1) has a -i option, then use it like this:

for i in *; do
sed -i 's/foo/bar/' $i
done

If not, there are several ways variations on the following depending on which language you want to play with:

ruby -i.bak -pe 'sub(%r{foo}, 'bar')' *
perl -pi.bak -e 's/foo/bar/' *

This appears to be what you want, based on the example you gave:

sed -i 's/foo/bar/g' *

It is not recursive (it will not descend into subdirectories). For a nice solution replacing in selected files throughout a tree I would use find:

find . -name '*.html' -print -exec sed -i.bak 's/foo/bar/g' {} \;

The *.html is the expression that files must match, the .bak after the -i makes a copy of the original file, with a .bak extension (it can be any extension you like) and the g at the end of the sed expression tells sed to replace multiple copies on one line (rather than only the first one). The -print to find is a convenience to show which files were being matched. All this depends on the exact versions of these tools on your system.

I like and used the above solution or a system wide search and replace among thousands of files:

find -name '*.htm?' -print -exec sed -i.bak 's/foo/bar/g' {} \;

I assume with the '*.htm?' instead of .html it searches and finds .htm and .html files alike.

I replace the .bak with the more system wide used tilde (~) to make clean up of backup files easier.

find . -type f -print0 | xargs -0 <sed/perl/ruby cmd> will process multiple space contained file names at once loading one interpreter per batch. Much faster.

This works using grep without needing to use perl or find.

grep -rli 'old-word' * | xargs -i@ sed -i 's/old-word/new-word/g' @

The answer already given of using find and sed

find -name '*.html' -print -exec sed -i.bak 's/foo/bar/g' {} \;

is probably the standard answer. Or you could use perl -pi -e s/foo/bar/g' instead of the sed command.

For most quick uses, you may find the command rpl is easier to remember. Here is replacement (foo -> bar), recursively on all files in the current directory:

rpl -R foo bar .

It's not available by default on most Linux distros but is quick to install (apt-get install rpl or similar).

However, for tougher jobs that involve regular expressions and back substitution, or file renames as well as search-and-replace, the most general and powerful tool I'm aware of is repren, a small Python script I wrote a while back for some thornier renaming and refactoring tasks. The reasons you might prefer it are:

  • Support renaming of files as well as search-and-replace on file contents (including moving files between directories and creating new parent directories).
  • See changes before you commit to performing the search and replace.
  • Support regular expressions with back substitution, whole words, case insensitive, and case preserving (replace foo -> bar, Foo -> Bar, FOO -> BAR) modes.
  • Works with multiple replacements, including swaps (foo -> bar and bar -> foo) or sets of non-unique replacements (foo -> bar, f -> x).

Check the README for examples.

This is actually easier than it seems.

grep -Rl 'foo' ./ | xargs -n 1 -I % sh -c "ls %; sed -i 's/foo/bar/g' %";
  • grep recurses through your tree (-R) and prints just the file name (-l), starting at the current directory (./)
  • that gets piped to xargs, which processes them one at a time (-n 1), and uses % as a placeholder (-I %) in a shell command (sh -c)
  • in the shell command, first the file name is printed (ls %;)
  • then sed does an inline operation (-i), a substution('s/') of foo with bar (foo/bar), globally (/g) on the file (again, represented by %)

Easy peasy. If you get a good grasp on find, grep, xargs, sed, and awk, almost nothing is impossible when it comes to text file manipulation in bash :)

2022 answer.
https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md#how-can-i-search-and-replace-with-ripgrep

rg foo --files-with-matches | xargs sed -i 's/foo/bar/g'

will replace all instances of 'foo' with 'bar' in the files in which ripgrep finds the foo pattern. The -i flag to sed indicates that you are editing files in place, and s/foo/bar/g says that you are performing a substitution of the pattern foo for bar, and that you are doing this substitution globally (all occurrences of the pattern in each file).