Combining Tests
Scope: joining tests into one expression —
side-by-side AND, -o for OR, -not and ! —
and the precedence that decides what an
expression actually says, including the trap
where -print joins only half of it.
AND: just stand them together
Two tests side by side both have to pass.
There is no keyword; adjacency is AND
(formally -a, almost never written):
$ find tree -type f -name '*.log'
tree/archive/recent.log
tree/archive/old.log
tree/archive/ancient.log
tree/big.log
Files, and named *.log. Most real find
commands are a chain of these: a type, a
name, an age, all narrowing one walk.
OR: -o
-o joins alternatives — either side may
pass:
$ find tree -name '*.jpg' -o -name '*.md'
tree/photos/img1.jpg
tree/report.md
One of the tree's quiet jokes is in that
listing: img2.JPG is missing, because as
chapter 2 established, -name '*.jpg' is
case-sensitive and nobody said -iname.
-o widens the net; it doesn't mend the
individual tests.
NOT: -not and !
-not (or a bare !, escaped from the shell
as \! where history expansion is watching)
inverts the test after it:
$ find tree/photos -not -type d
tree/photos/raw/img3.CR2
tree/photos/img1.jpg
tree/photos/img2.JPG
Everything in photos that isn't a directory.
Note the raw file sails through a -not
whose job was only about types — NOT applies
to the one test it precedes, nothing wider,
unless you group, which is next.
Precedence, and the -print trap
AND binds tighter than OR: a -o b c reads
as a, or (b and c). Usually that's what
you meant. But actions are part of the
expression too, evaluated in the same left-to-
right flow — and here is the capture this
chapter exists for. It looks like "print
the text files and the markdown file":
$ find tree -name '*.txt' -o -name '*.md' -print
tree/report.md
Only the markdown. Here is why. The expression is
-name '*.txt' -o ( -name '*.md' -a -print )
-print stands on the right of the -o, so
it is part of the right-hand alternative:
entries matching *.md are printed, entries
matching *.txt merely pass — and pass to
nothing. Chapter 1 said find adds a -print
of its own when an expression has no action;
this expression has an action, so no
automatic one is added, and the fifteen text
files were selected and silently discarded.
How many were lost, for the record:
$ find tree -type f -name '*.txt' | wc -l
15
The fix is to group with parentheses, so the action stands outside the choice. Parentheses are shell metacharacters, so they are escaped with backslashes (or the whole expression piece quoted):
$ find tree \( -name '*.txt' -o -name '*.md' \) -print
tree/subdir/nested.txt
tree/times/fresh.txt
tree/times/halfhour.txt
tree/times/marker.txt
tree/times/yesterday.txt
tree/times/twodays.txt
tree/perms/world-writable.txt
tree/perms/secret.txt
tree/perms/normal.txt
tree/perms/not-mine.txt
tree/notes.txt
tree/report.md
tree/.hidden.txt
tree/empty.txt
tree/meeting notes.txt
tree/o'brien.txt
Sixteen lines: fifteen text files and
report.md, the answer the author of the
unparenthesised version believed they were
getting. Grouping also composes with NOT —
! \( a -o b \) is "neither" — and with
longer chains, and the rule for reading any
expression you didn't write this morning is
simple: find where the actions stand, draw
the parentheses the author didn't, and
only then decide what the command does.
This trap is placed here, before the actions chapter, on purpose. Nothing in the broken command was exotic — two ordinary tests and the most ordinary action — and its failure was silent, complete, and plausible-looking. Remember its shape when chapter 8 gives actions teeth.