130

When I'm using

find . -type f -name "*.htm*" -o -name "*.js*" -o -name "*.txt"

it finds all the types of file. But when I add -exec at the end:

find . -type f -name "*.htm*" -o -name "*.js*" -o -name "*.txt" -exec sh -c 'echo "$0"' {} \;

it seems it only prints .txt files. What am I doing wrong?

Note: using MINGW (Git Bash)

AdminBee
  • 22,803
jakub.g
  • 3,263
  • 5
  • 21
  • 18

2 Answers2

170
find . -type f -name "*.htm*" -o -name "*.js*" -o -name "*.txt"

is short for:

find . '('                                      \
           '(' -type f -a -name "*.htm*" ')' -o \
           '(' -name "*.js*" ')' -o             \
           '(' -name "*.txt" ')'                \
       ')' -a -print

That is, because no action predicate is specified (only conditions), a -print action is implicitly added for the files that match the conditions.

(and, by the way, that would print non-regular .js files (the -type f only applies to .htm files)).

While:

find . -name "*.htm*" -o -name "*.js*" -o -name "*.txt" \
  -exec sh -c 'echo "$0"' {} \;

is short for:

find . '(' -type f -a -name "*.htm*" ')' -o \
       '(' -name "*.js*" ')' -o \
       '(' -name "*.txt" -a -exec sh -c 'echo "$0"' {} \; ')'

For find (like in many languages), AND (-a; implicit when omitted) has precedence over OR (-o), and adding an explicit action predicate (here -exec) cancels the -print implicit action seen above.

Here, you want:

find . '(' -name "*.htm*" -o -name "*.js*" -o -name "*.txt" ')' \
       -type f \
       -exec sh -c 'echo "$0"' {} \;

Or:

find . '(' -name "*.htm*" -o -name "*.js*" -o -name "*.txt" ')' \
       -type f \
       -exec sh -c '
         for i do
           echo "$i"
         done' sh {} +

To avoid running one sh per file.

(-type f being more expensive than -name as it potentially implies retrieving information from the inode, is best put after though some find implementations do reorder the checks internally for optimisation).

41

It is the implied brackets. Add explicit brackets. \( \)

find . -type f \( -name "*.htm*" -o -name "*.js*" -o -name "*.txt" \) -exec sh -c 'echo "$0"' {} \;

or using xargs ( I like xargs I find it easier, but apparently not as portable).

find . -type f \( -name "*.htm*" -o -name "*.js*" -o -name "*.txt" \) -print0 | xargs -0 -n1 echo