7

Here's my command (break intentional):

grep FOO "/Users/gjtorikian/blah" -l | xargs sed -i '' '/FOO/{s/FOO/BAR/g; w /dev/stdout
}'

At the high-level: grep for FOO in the blah directory; pipe in just the filename (because of -l) to sed; sed performs an inline replace (-i '') and prints only the changed term to /dev/stdout.

If I were to omit the -l and pipe, I get this back from grep:

/Users/gjtorikian/blah/baz.cs:1:FOO
/Users/gjtorikian/blah/bar.js:1:FOO

What I want is sed to perform the inline replace, and then show me the file and term replaced; for example:

/Users/gjtorikian/blah/baz.cs:1:BAR
/Users/gjtorikian/blah/bar.js:1:BAR

Is such a thing possible? If it matters, I would prefer to keep it with only grep/sed. Do I have to do a second grep after the sed ?

3
  • I don't see a way to get the current file name in sed, so you probably will need a second grep.
    – Kevin
    Feb 25, 2012 at 20:50
  • Also, you don't need the '', that's the default.
    – Kevin
    Feb 25, 2012 at 20:51
  • 3
    On OS X, '' is not the default.
    – gjtorikian
    Feb 25, 2012 at 22:57

5 Answers 5

4

One possibility would be to pass the output of grep to a separate sed filter.

grep -l FOO "/Users/gjtorikian/blah/"* |
{ tee /dev/fd/3 |
  xargs sed -i -e '/FOO/{' -e 's/FOO/BAR/g' -e 'w /dev/stdout'
} 3>&1 | sed 's/:FOO$/:BAR/'

You could make sed print the line number (with the = command) when it finds a match and do further postprocessing.

It would probably be clearer to use awk.

for x in "/Users/gjtorikian/blah/"*; do
  awk '
    sub(/FOO/, "BAR") {found=1; print FILENAME ":" NR ":" "BAR" >"/dev/stderr"}
    1 {print}
    END {exit(!found)}
' "$x" >"$x.tmp" && mv "$x.tmp" "$x"
done
3

As I'm not native English speaker I probably didn't get it.

To 'grep' a directory you need '-r'. Usage of '-l' prints just filename and it stops grepping after first occurence.

# pattern=/home ; grep -l "$pattern" /etc/[a-z]* 2>/dev/null | while read line ; do echo "$line:$pattern" ; done
/etc/adduser.conf:/home
/etc/fstab:/home
/etc/fstab~:/home
/etc/libuser.conf:/home
/etc/mpd.conf:/home
/etc/mpd.conf~:/home
/etc/mtab:/home
/etc/netscsid.conf:/home
/etc/passwd:/home
/etc/passwd-:/home
/etc/sudoers:/home
/etc/sudoers.tmp~:/home
2

Recursively replace FOO with BAR in current dir (quiet mode)

grep -rl 'FOO' | xargs sed -i 's/FOO/BAR/'
1

I think you're making the problem too complicated by trying to do it in one go. You simply can do a

grep -H -n /Users/gjtorikian/blah | sed 's/\(^.*?:[0-9]+?:\).*FOO.*/\1BAZ/'

to get the list of files with line numbers and replacements (this should work, as long as your filenames don't contain colons, but that's a bad idea in Mac OS anyway...). Afterwards you can issue an

sed -i '' 's/FOO/BAR/g' /Users/gjtorikian/blah

No grep and xarg is needed here (you may do a "find ... | xarg" if you have a lot of files though). If you're concerned about the duplications, you can put the two lines into a script or function and do variable substitutions there.

2
  • sed -i ... changes the filetimes even if the stream is unchanged.
    – user56041
    Nov 5, 2015 at 9:23
  • @jww: you're right, that's because sed is a stream editor not a file editor, so the -i option violates unix philosophy (mywiki.wooledge.org/BashFAQ/021#Using_nonstandard_tools). If you want to avoid that use ex (e.g. ex -sc '%s/FOO/BAR/ge|x' /Users/gjtorikian/blah). Nov 13, 2015 at 9:51
0

One liner

grep -n FOO * -R|awk -F\: '{ print $1 }'|sort|uniq|while read file; do sed -i 's/FOO/BAR/g' $file; done

You must log in to answer this question.

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