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 pruned.

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.

Tags:

Posix

Find