Limit POSIX find to specific depth?
@meuh's approach is inefficient as his -maxdepth 1
approach still lets find
read the content of directories at level 1 to later ignore them otherwise. It will also not work properly with some find
implementations (including GNU find
) if some directory names contain sequences of bytes that don't form valid characters in the user's locale (like for file names in a different character encoding).
find . \( -name . -o -prune \) -extra-conditions-and-actions
is the more canonical way to implement GNU's -maxdepth 1
(or FreeBSD's -depth -2
).
Generally though, it's -depth 1
you want (-mindepth 1 -maxdepth 1
) as you don't want to consider .
(depth 0), and then it's even simpler:
find . ! -name . -prune -extra-conditions-and-actions
For -maxdepth 2
, that becomes:
find . \( ! -path './*/*' -o -prune \) -extra-conditions-and-actions
And that's where you run in the invalid character issues.
For instance, if you have a directory called Stéphane
but that é
is encoded in the iso8859-1 (aka latin1) charset (0xe9 byte) as was most common in Western Europe and the America up until the mid 2000s, then that 0xe9 byte is not a valid character in UTF-8. So, in UTF-8 locales, the *
wildcard (with some find
implementations) will not match Stéphane
as *
is 0 or more characters and 0xe9 is not a character.
$ locale charmap
UTF-8
$ find . -maxdepth 2
.
./St?phane
./St?phane/Chazelas
./Stéphane
./Stéphane/Chazelas
./John
./John/Smith
$ find . \( ! -path './*/*' -o -prune \)
.
./St?phane
./St?phane/Chazelas
./St?phane/Chazelas/age
./St?phane/Chazelas/gender
./St?phane/Chazelas/address
./Stéphane
./Stéphane/Chazelas
./John
./John/Smith
My find
(when the output goes to a terminal) displays that invalid 0xe9 byte as ?
above. You can see that St<0xe9>phane/Chazelas
was not prune
d.
You can work around it by doing:
LC_ALL=C find . \( ! -path './*/*' -o -prune \) -extra-conditions-and-actions
But note that that affects all the locale settings of find
and any application it runs (like via the -exec
predicates).
$ LC_ALL=C find . \( ! -path './*/*' -o -prune \)
.
./St?phane
./St?phane/Chazelas
./St??phane
./St??phane/Chazelas
./John
./John/Smith
Now, I really get a -maxdepth 2
but note how the é in the second Stéphane properly encoded in UTF-8 is displayed as ??
as the 0xc3 0xa9 bytes (considered as two individual undefined characters in the C locale) of the UTF-8 encoding of é are not printable characters in the C locale.
And if I had added a -name '????????'
, I would have gotten the wrong Stéphane (the one encoded in iso8859-1).
To apply to arbitrary paths instead of .
, you'd do:
find some/dir/. ! -name . -prune ...
for -mindepth 1 -maxdepth 1
or:
find some/dir/. \( ! -path '*/./*/*' -o -prune \) ...
for -maxdepth 2
.
I would still do a:
(cd -P -- "$dir" && find . ...)
First because that makes the paths shorter which makes it less likely to run into path too long or arg list too long issues but also to work around the fact that find
can't support arbitrary path arguments (except with -f
with FreeBSD find
) as it will choke on values of $dir
like !
or -print
...
The -o
in combination with negation is a common trick to run two independent sets of -condition
/-action
in find
.
If you want to run -action1
on files meeting -condition1
and independently -action2
on files meeting -condition2
, you cannot do:
find . -condition1 -action1 -condition2 -action2
As -action2
would only be run for files that meet both conditions.
Nor:
find . -contition1 -action1 -o -condition2 -action2
As -action2
would not be run for files that meet both conditions.
find . \( ! -condition1 -o -action1 \) -condition2 -action2
works as \( ! -condition1 -o -action1 \)
would resolve to true for every file. That assumes -action1
is an action (like -prune
, -exec ... {} +
) that always returns true. For actions like -exec ... \;
that may return false, you may want to add another -o -something
where -something
is harmless but returns true like -true
in GNU find
or -links +0
or -name '*'
(though note the issue about invalid characters above).
You can use -path
to match a given depth and prune there. Eg
find . -path '*/*/*' -prune -o -type d -print
would be maxdepth 1, as *
matches the .
, */*
matches ./dir1
, and */*/*
matches ./dir1/dir2
which is pruned. If you use an absolute starting directory you need to add a leading /
to the -path
too.