HOWTO: Performing global search and replace across files on your Linux machine

Posted : July 6, 2004 at 1:40 pm [America/Los_Angeles]

Version History:

Date: 07/06/2004

Version: 0.3

Notes: Edited the entry to add a version showing how to parse files in sub-folders as well. Thanks to Wei (see comments below) for pointing this out.

Version History:

Date: 07/06/2004

Version: 0.2

Notes: Edited the entry to seperate out two solutions - Bash/Sed based and Perl based. Thanks to MS (see comments below) for pointing this out.

Date: 07/06/2004

Version: 0.1

Notes: Original post

Was assigned a simple task this morning:

...
<a href="http://indrayam.com">Back to Home</a>
</body>
</html>

There are lots of files in a directory that have a pattern similar to the one shown above. My assignment was to replace the URL in the href entries with a new one.

Sounds like a fairly common and simple problem. However, I soon realized that I did not have a canned utility or shell script that I could quickly use. After some googling around, here’s what I came up with:

Bash/Sed version:

#!/bin/bash

for i in $( ls *.html ); do
   sed "s/\"http://indrayam.com\"/\"http://indrayam.com/it/\"/g" $i > $i.new
   mv -f $i.new $i
done

If you want to peruse through content a few levels deep, try this:

#!/bin/bash

for i in $( find . -name '*.html' ); do
   sed "s/\"http://indrayam.com\"/\"http://indrayam.com/it/\"/g" $i > $i.new
   mv -f $i.new $i
done

Here are a few pointers, in case you do decide on using the script:

  1. For starters, restrict the $(ls *.<file-extension>) section of the shell script to a single file by replacing it with $(ls <filename>). In other words, test the script out on a single file first.

  2. Always run the “double-whammy” test - Run the shell script once and then run the script again without any modifications!. If the targetted file looks ok after this double execution, you have wisely selected your initial-pattern and new-pattern in sed’s s/<initial-pattern>/<new-pattern>/g expression.

    For example, let’s say the sed expression in the shell script shown above looked something like this instead:

    sed "s/http://indrayam.com/http://indrayam.com/it//g" $i > $i.new
    

    Do you see a problem here? Since our initial-pattern is now http://indrayam.com (as opposed to “http://indrayam.com” in the shell script above), executing the script against the same file twice will result in href looking something like:

    <a href="http://indrayam.com/it//it/">Back to Home</a>
    

    Of course, this double-whammy problem will bite you primarily if your initial-pattern is a subset of the new-pattern

  3. Only after you’ve tested for these two things should you consider opening up the spigot by resetting $(ls <filename>) to $(ls *.<file-extension>)

Note:

If you’re looking for a backup of the original file, you might want to tweak the shell script above as follows:

#!/bin/bash

for i in $( ls *.html ); do
   cp $i $i.bak
   sed "s/\"http://indrayam.com\"/\"http://indrayam.com/it/\"/g" $i > $i.new
   mv -f $i.new $i
done

Perl One-liner:

If you like Perl, here’s a really, really neat one-liner:

(unix prompt)>perl -i.bak -p -e 's#"http://indrayam.com"#"http://indrayam.com/it/"#ig' *.html

If you want to peruse through content a few levels deep, try this:

(unix prompt)>perl -i.bak -p -e 's#"http://indrayam.com"#"http://indrayam.com/it/"#ig' {.,*,*/*,*/*/*}/*.html

Nice!

For more information, check out this site Thanks to MS (see comments below) for pointing this out.

Note:

The last perl one-liner example shown above will parse files in current dir and all subdirs three levels deep. I am not sure what would be the easiest way to parse files at an undefined depth level using perl one-liner without making it insanely cryptic. Any ideas?

- Anand

Viewed: 2460 times

11 Comments

What’s the difference between “for i in $( ls *.html ); …” and “for i in *.html;”? Wouldn’t that work too?

Posted by: Janek Schwarz at July 6, 2004 @ 2:47 pm

try searching on “perl -eip” and look at pages like:

http://www.rice.edu/web/perl-edit.html

It assumes you ahve perl but well I assume that most unix installs do these days.

Posted by: MS at July 6, 2004 @ 2:49 pm

Another utility you might find quite useful is supersed (http://queen.rett.polimi.it/~paolob/seders/ssed/). With its -R option, it allows you to use Perl-like regular expressions. Not as quirky as sed’s ;)

Posted by: Maik at July 6, 2004 @ 3:03 pm

Whoops, make that http://sed.sourceforge.net/grabbag/ssed/ — I guess Gentoo has an outdated URL there :-/

Posted by: Maik at July 6, 2004 @ 3:05 pm

MS:

I checked out the Perl site at Rice. Super stuff and thank you for sharing! Considering that I am a huge Perl fan myself, I could not have asked for anything better.

Now, I just have to remember the command-line arguments..;-)

Thanks again!

Posted by: Anand Sharma at July 6, 2004 @ 3:13 pm

Janek:

Clarity is one reason I would go with a $() construct. But then again, that’s just me..:-)

- Anand

Posted by: Anand Sharma at July 6, 2004 @ 3:14 pm

Thanks Maik. Will check it out.

- Anand

Posted by: Anand Sharma at July 6, 2004 @ 3:16 pm

Heck! This article is a day late! I was looking yesterday some pointers for sed (I am still weak in regex)…
Finally, I just got down a few hours hacking searchnreplace.java this morning!

Posted by: hishammk at July 6, 2004 @ 7:41 pm

To recursively go through all directories and its sub-directories, you probably will do something like:

for i in ( find . -name ‘*.html’ ); do

done

Posted by: wei at July 6, 2004 @ 8:04 pm

Very good point, Wei. I will make sure I add that to the entry.

- Anand

Posted by: Anand Sharma at July 6, 2004 @ 8:27 pm

#!/bin/bash
replace “domain.com” “new-domain.com” — /path/to/your/files/*.ext

Might help :)

Thanks all

Posted by: Mohamed at January 3, 2007 @ 9:20 am