33

Is there a way to logically combine two shell commands that are invoked with find - exec?

For instance to print out all the .csv files that contain the string foo together with its occurrence I would like to do:

find . -iname \*.csv -exec grep foo {} && echo {} \;

but bash complains with "missing argument to '-exec' "

Marcus Junius Brutus
  • 4,587
  • 11
  • 44
  • 65
  • 3
    You could use 2 -exec in sequence or use a single -exec sh -c 'grep foo "$0" && printf %s\\n "$0"' {} \;. – jw013 Nov 13 '12 at 19:03
  • This has tripped me up repeatedly: I always expect that the first argument passed to sh (in this case {}) will be $1 and $0 will be something like sh. But in fact, you are correct, the first argument shows up as $0. Having the first argument be the name of the invoking command is just a convention, that isn't automatically enforced in these cases. – dubiousjim Nov 13 '12 at 19:38
  • Maybe should be merged with this question: http://unix.stackexchange.com/q/18077/4801 – dubiousjim Nov 14 '12 at 16:02

3 Answers3

31

-exec is a predicate that runs a command (not a shell) and evaluates to true or false based on the outcome of the command (zero or non-zero exit status).

So:

find . -iname '*.csv' -exec grep foo {} \; -print

would print the file path if grep finds foo in the file. Instead of -print you can use another -exec predicate or any other predicate

find . -iname '*.csv' -exec grep foo {} \; -exec echo {} \;

See also the ! and -o find operators for negation and or.

Alternatively, you can start a shell as:

find . -iname '*.csv' -exec sh -c '
   grep foo "$1" && echo "$1"' sh {} \;

Or to avoid having to start a shell for every file:

find . -iname '*.csv' -exec sh -c '
  for file do
    grep foo "$file" && echo "$file"
  done' sh {} +
11

The problem you're facing is that the shell first parses the command line, and sees two simple commands separated by the && operator: find . -iname \*.csv -exec grep foo {}, and echo {} \;. Quoting && (find . -iname \*.csv -exec grep foo {} '&&' echo {} \;) bypasses that, but now the command executed by find is something like grep with the arguments foo, wibble.csv, &&, echo and wibble.csv. You need to instruct find to run a shell that will interpret the && operator:

find . -iname \*.csv -exec sh -c 'grep foo "$0" && echo "$0"' {} \;

Note that the first argument after sh -c SOMECOMMAND is $0, not $1.

You can save the startup time of a shell process for every file by grouping the command invocations with -exec … +. For ease of processing, pass some dummy value as $0 so that "$@" enumerates the file names.

find . -iname \*.csv -exec sh -c 'for x in "$@"; do grep foo "$x" && echo "$x"; done' \ {} +

If the shell command is just two programs separated by &&, find can do the job by itself: write two consecutive -exec actions, and the second one will only be executed if the first one exits with the status 0.

find . -iname \*.csv -exec grep foo {} \; -exec echo {} \;

(I assume that grep and echo are just for illustration purpose, as -exec echo can be replaced by -print and the resulting output is not particularly useful anyway.)

  • 2
    I tend to avoid using "$0" for that, as it's also used by the shell to display error messages. For instance, you could see a confusing ./some-file: grep: command not found error message. -exec find sh -c '... "$1"' sh {} \; wouldn't have the issue. There's a typo (related) in your second find command. – Stéphane Chazelas Nov 14 '12 at 09:31
4

In this specific case I would do:

find . -iname \*.csv -exec grep -l foo \{\} \;

Or if you have ack:

ack -al -G '.*\.csv' foo

To answer your actual question, something like this may work:

find . -iname \*.csv -exec sh -c "grep foo {} && echo {}" \;