0

I love to use the following command to do find / replace across multiple files in bash:

find -wholename "*.txt" -print | xargs sed -i 's/foo/bar/g'

However, the above command process everything in silence, and sometimes I would like the above command to print all the changes it made in order to double check if I did everything correctly. Can I know how should I improve the above command to allow it to dump such information? I tried the -v argument in the xargs command but it gives me the invalid option error.

keelar
  • 5,814
  • 7
  • 40
  • 79
  • 1
    you could use emacs for that: [Using Emacs to recursively find and replace in text files not already open](http://stackoverflow.com/q/270930/4279). – jfs Jun 26 '13 at 05:04
  • Thank you @J.F.Sebastian for the information, but I am a vim guy :~ – keelar Jun 26 '13 at 05:06
  • You could remove `-i` and redirect everything to a file. If everything is ok, execute your original command. Also see [sed command in dry run](http://stackoverflow.com/questions/4513623/sed-command-in-dry-run) – devnull Jun 26 '13 at 05:07
  • @devnull: thank you for the pointer. I just tried it but seems like it outputs the whole files instead of just the replaced part @@", any idea of how to fix that @@" – keelar Jun 26 '13 at 05:09
  • To all awesome people who answered my question: I'm sorry that I have ran out of my votes today, I will vote up your answer once I can vote again :~~ – keelar Jun 26 '13 at 05:25

3 Answers3

1

You can do something like:

find -wholename "*.txt" | xargs sed -n '/foo/p;s/foo/bar/gp'

What this will do is print the line that you wish to substitute and print the substitution in the next line.

You can use awk and get filename as well:

find -wholename "*.txt" | xargs awk '/foo/{print FILENAME; gsub(/foo/,"bar");print}'

To print entire file remove print and add 1

find -wholename "*.txt" | xargs awk '/foo/{print FILENAME; gsub(/foo/,"bar")}1'

Regex will have to be modified as per your requirement and changes in-file is only available in gawk version 4.1

Test:

$ head file*
==> file1 <==
,,"user1","email"
,,"user2","email"
,,"user3","email"
,,"user4","email"

==> file2 <==
,,user2,location2
,,user4,location4
,,user1,location1
,,user3,location3
$ find . -name "file*" -print | xargs awk '/user1/{print FILENAME; gsub(/user1/,"TESTING");print}'
./file1
,,"TESTING","email"
./file2
,,TESTING,location1
 
Community
  • 1
  • 1
jaypal singh
  • 74,723
  • 23
  • 102
  • 147
  • Thank you for your answer. But the output has the same issue as what my solution has, is there a way to know which file contains the output? – keelar Jun 26 '13 at 05:17
  • @keelar Added `awk` solution to print filename – jaypal singh Jun 26 '13 at 05:18
  • Thank you, I am trying your solution, but have syntax error the `print` right after `/foo/{`. Did I do something wrong? – keelar Jun 26 '13 at 05:21
  • 1
    @keelar Added a sample test – jaypal singh Jun 26 '13 at 05:25
  • Thank you for the sample test. I tried the sample test but it seems that it does not actually change the files but a dry-run instead. Is that what you mentioned at the end of your answer? – keelar Jun 26 '13 at 05:29
  • Yes, you can do `in-file` changes however that is only available in `awk` version 4.1 – jaypal singh Jun 26 '13 at 05:33
  • Too bad, mine is 3.1.5 :~ Suppose I have v4.1, can I know what regex should I put? – keelar Jun 26 '13 at 05:35
  • 1
    Sure, `find -wholename "*.txt" | xargs gawk -i inplace '/foo/{print FILENAME; gsub(/foo/,"bar");print}'` – jaypal singh Jun 26 '13 at 05:37
  • Great thank !! I will +1 once I am able to, still struggling with one line solution runnable on my current environment :~~ – keelar Jun 26 '13 at 05:39
  • @JS웃: `xargs` is not really needed. `find` have the same feature internally. See `-exec ... +`. – TrueY Jun 27 '13 at 05:57
1

In order to see the differences you can redirect the output of sed to a new file for every input file and compare it with the original.

for i in `find -wholename "*.txt"`; do
  sed 's/foo/bar/g' ${i} > ${i}.new;
  diff -u ${i} ${i}.new;
done

If the changes seem ok, move the new files to their original names.

for i in `find -wholename "*.new"` ; do
  mv ${i} ${i/.new};
done
devnull
  • 118,548
  • 33
  • 236
  • 227
1

All can be done with and . Only a little modification needed:

find -path "*.txt" -exec sed -i.bak 's/foo/bar/g' {} +

This calls with the max number of files (mind + at the end of -exec), so is not needed. In -i.bak does an in-place-editing renaming the original file as .bak. So You can check the differences later if needed.

In man find one can read:

-wholename pattern
      See -path.    This alternative is less portable than -path.
TrueY
  • 7,360
  • 1
  • 41
  • 46