31

When using sed to replace strings in-place, is there a way to make it report the changes it does (without relying on a diff of old and new files)?

For instance, how can I change the command line

find . -type f | xargs sed -i 's/abc/def/g'

so I can see the changes that are made on the fly?

5 Answers 5

23

You could use sed's w flag with either /dev/stderr, /dev/tty, /dev/fd/2 if supported on your system. E.g. with an input file like:

foo first
second: missing
third: foo
none here

running

sed -i '/foo/{
s//bar/g
w /dev/stdout
}' file

outputs:

bar first
third: bar

though file content was changed to:

bar first
second: missing
third: bar
none here

So in your case, running:

find . -type f -printf '\n%p:\n' -exec sed -i '/foo/{
s//bar/g
w /dev/fd/2
}' {} \;

will edit the files in-place and output:

./file1:
bar stuff
more bar

./file2:

./file3:
bar first
third: bar

You could also print something like original line >>> modified line e.g.:

find . -type f -printf '\n%p:\n' -exec sed -i '/foo/{
h
s//bar/g
H
x
s/\n/ >>> /
w /dev/fd/2
x
}' {} \;

edits the files in-place and outputs:

./file1:
foo stuff >>> bar stuff
more foo >>> more bar

./file2:

./file3:
foo first >>> bar first
third: foo >>> third: bar
0
21

You could do it in two passes using the print action on the first pass with:

find . -type f | xargs sed --quiet 's/abc/def/gp'

where --quiet makes sed not show every line and the p suffix shows only lines where the substitution has matched.

This has the limitation that sed will not show which files are being changed which of course could be fixed with some additional complexity.

2
  • It doesn't quite do what I wanted since you need two passes, but I'm voting up because I just learned this p thing and it's very useful. Especially if, as you said, the files were also printed. Something like for x in `find . -type f`; do echo ///File $x: ; sed --quiet 's/abc/def/gp' $x; done
    – ricab
    Commented Oct 23, 2013 at 18:34
  • @msw I have many sed expression, I don't want to write /gp for each one. How to set it globaly?
    – alhelal
    Commented Jan 7, 2018 at 1:30
9

I don't think that's possible, but a workaround might be to use perl instead:

find . -type f -exec perl -i -pe 's/abc/def/ && print STDERR' {} +

This will print the altered lines to standard error. For example:

$ cat foo
fooabcbarabc
blah blah
$ find . -type f -exec perl -i -pe 's/abc/def/ && print STDERR' {} +
foodefbarabc
$ cat foo
foodefbarabc
blah blah

You can also make this slightly more complex, printing the line number, file name, original line and changed line:

$ find . -type f -exec perl -i -pe ' 
   $was=$_; chomp($was);
   s/abc/def/ && print STDERR "$ARGV($.): $was : $_"
   close ARGV if eof' {} +
./foo(1): fooabcbarabc : foodefbarabc

Note the close ARGV if eof which is needed for $. to be reset between each file.

Add the g flag to the s/// operator to replace all occurrences on each line rather than just the first.

4
  • +1 You can also use $ARGV for the name of the file being operated on.
    – Joseph R.
    Commented Oct 23, 2013 at 18:38
  • @JosephR. good point, added.
    – terdon
    Commented Oct 23, 2013 at 18:46
  • Thanks, that's probably helpful (didn't test it myself). I am not selecting as it doesn't use sed, so it doesn't exactly answer the question.
    – ricab
    Commented Oct 23, 2013 at 20:06
  • @ricab that's fine, you should not accept an answer unless it actually answers your question. Bear in mind though that for simple things like this, perl's syntax is very similar to sed's and I don't think what you're asking for is actually possible with sed.
    – terdon
    Commented Oct 23, 2013 at 20:14
5

It is possible using the w flag which writes the current pattern to a file. So by adding it to the substitute command we can report successive substitutions to a file and print it after the job is done. I also like to colorize the replaced string with grep.

sed -i -e "s/From/To/gw /tmp/sed.done" file_name
grep --color -e "To" /tmp/sed.done

Note, that there must be only one space between the w and its file name.

This is even better than diff, since diffing might also show changes even if they were not made by sed.

1
  • 1
    Just tested it out and sed only writes lines it substituded to sed.done. The lines with "To" in the original file are therefore not printed to sed.done, thus when you grep "To" sed.done you will only see the lines that are changed by sed. You won't see the original line in the file before it was substituted, if that's what you're aiming at ...
    – aairey
    Commented Feb 19, 2016 at 10:34
1

I like the @terdon solution - perl is good for this.

Here's my tweaked version that:

  • won't try to alter files that dont have have the matching string
  • will backup the before version of the files that change (create a .bak version)
  • will list every file/lineno changed, and show the OLD and NEW versions of that line indented and underneath for easy reading

code

find /tmp/test -type f ! -name "*.bak" -exec grep -l '/opt/gridmon' {} \; | xargs -L1 perl -ni'.bak' -e'$old=$_; s/\/opt\/gridmon/~/g && print STDERR "$ARGV($.):\n\tOLD:$old\tNEW:$_"'

example output

/tmp/test/test4.cfg(13):
    OLD:    ENVFILE /opt/gridmon/server/etc/gridmonserver.cfg
    NEW:    ENVFILE ~/server/etc/gridmonserver.cfg
/tmp/test/test4.cfg(24):
    OLD:    ENVFILE /opt/gridmon/server/etc/gridmonserver.cfg
    NEW:    ENVFILE ~/server/etc/gridmonserver.cfg

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .