grep piping into sed, replacing inline; but I want sed to print the filename and changed line. Is it possible?

52,312

Solution 1

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

Solution 2

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

Solution 3

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.

Solution 4

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

grep -rl 'FOO' | xargs sed -i 's/FOO/BAR/'
Share:
52,312
gjtorikian
Author by

gjtorikian

Updated on September 18, 2022

Comments

  • gjtorikian
    gjtorikian over 1 year

    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 ?

    • Admin
      Admin about 12 years
      I don't see a way to get the current file name in sed, so you probably will need a second grep.
    • Admin
      Admin about 12 years
      Also, you don't need the '', that's the default.
    • Admin
      Admin about 12 years
      On OS X, '' is not the default.
  • Admin
    Admin over 8 years
    sed -i ... changes the filetimes even if the stream is unchanged.
  • ajkal
    ajkal over 8 years
    @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).