4

I have a program called foo which I want to exec on each result that is found via find. So something like this:

find . -name '*.o' -type f -exec foo {} \;

I want to grep the output of all of these invocations of foo for a certain string bar. So I add this:

find . -name '*.o' -type f -exec foo {} \; | grep bar

But I lose the original information about what files the matches are coming from. I tried adding -fprintf /dev/stderr '%p\n' to the find command but now it looks like stdout is gone, because no grep results are printed.

How can I get each filename printed to the output and THEN have the grep results corresponding to that file printed after?

Alternatively, if there were some way to make the -H argument of grep work that would be fine too, but as written it won't work because I'm just passing text from stdin, and grep doesn't know the filename. I tried various incantations of xargs, but I couldn't get any of that to work either.

1
  • Does your grep support a --label option? Dec 15, 2017 at 19:27

3 Answers 3

3

To get each filename printed to the output and THEN have the grep results corresponding to that file printed after, you could wrap the -exec foo {} | grep pipe in a shell:

find . -name '*.o' -type f -print -exec sh -c 'foo "$1" | grep "bar"' sh {} \;

To to make the -H argument of grep work with standard input, if your version of grep supports the --label= option you could do

find . -name '*.o' -type f -exec sh -c '
  foo "$1" | grep -H --label="$1" "bar"
' sh {} \;

or (if your find supports the + multi-argument alternative to \;):

find . -name '*.o' -type f -exec sh -c '
  for f; do foo "$f" | grep -H --label="$f" "bar"; done
' sh {} +
2
  • This is probably a dumb question but what is $1? Dec 15, 2017 at 21:54
  • @ZacharyTurner $1 is the first argument (positional parameter) passed to the sh -c shell command; in effect, {} outside the shell becomes $1 inside the shell when we use find . . . {} \; (and becomes a parameter list $@ when we use the find . . . + GNU extension). Dec 15, 2017 at 22:29
2

How can I get each filename printed to the output and THEN have the grep results corresponding to that file printed after?

1.) use two instances of grep, one for printing the filename and another one for printing the match:

find . -name '*.o' -type f -exec grep -l bar {} \; -exec grep bar {} \;

2.) make grep behave as if it was multiple files:

find . -name '*.o' -type f -exec grep bar {} /dev/null \;

this does not solve the problem with [..]-exec foo {} \;[...] and running grep within a pipe as in your example.

1

This is pretty ugly, but it will work for you:

find . -name '*.o' -type f -exec sh -c 'foo "$1" | sed "s@^@$1\t@"' _ {} \;

That will put the filename and a tab at the start of each line. The bare underscore is a string that populates $0 in the spawned shell, leaving the filename as $1. Now you can grep the output of find, and the filename will appear in the output.


That will break for any filenames containing the @ character. If that's a concern, we can augment the shell script:

find . -name '*.o' -type f -exec sh -c '
    r=$(echo "$1" | sed "s/@/\\\\\\\\@/g")
    foo "$1" | sed "s@^@$r\t@"
' _ {} \;

8 backslashes were determined by trial and error ...

Or using bash (or ksh) which has a convenient "search and replace" parameter substitution

find . -name '*.o' -type f -exec ksh -c 'foo "$1" | sed "s@^@${1//@/\\\\@}\t@"' _ {} \;

You must log in to answer this question.

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