Find files safely with GNU find

A find command that looks right can still match far more than you meant, and with -delete on the end there is no undo. In about 15 minutes you will build find commands that stay in scope, match what you intend and survive odd filenames.

The examples use GNU findutils 4.9.0. Allow a few extra minutes if you are adapting them to a large filesystem.

1. Set the starting point and check the scope

The first arguments are starting points; the expression follows them. Omit the starting point and GNU find searches the current directory, .. Keep that split visible, because a path beginning with - can otherwise be mistaken for part of the expression.

$ find /path/to/project -maxdepth 2 -type f -print
/path/to/project/README.md
/path/to/project/src/main.c
/path/to/project/src/notes.txt

Checkpoint: Before adding any action, run with -print and check every path is inside the tree you meant. -print is also the default when no action is given, but writing it out makes the command easier to review.

2. Match names and file types

-name matches the final path component and is case-sensitive; -iname ignores case. Quote wildcard patterns so the shell hands them to find instead of expanding them against the current directory first.

$ find /path/to/project -type f -name '*.c' -print
/path/to/project/src/main.c

Group conditions with escaped parentheses. The shell never sees them, and the grouping means either suffix is accepted:

$ find /path/to/project -type f \( -name '*.c' -o -name '*.h' \) -print

No operator between tests means AND. -o means OR and binds more loosely than AND.

Warning: In -name 'one' -o -name 'two' -print, only the second branch has the print action. Group the whole condition when both matches should be acted on.

3. Narrow by time, size or permissions

Time tests count complete 24-hour periods, not calendar days.

$ find /path/to/project -type f -mtime -1 -print
/path/to/project/src/main.c

Other useful read-only tests: -empty, -size +10M and -perm -u+x.

Warning: Check the exact predicate in man find before it goes into a cleanup job. Size suffixes and permission forms have specific meanings, and a plausible-looking command can select a much larger set than you expect.

Checkpoint: Save a representative listing before changing anything. Counting it modifies nothing:

$ find /path/to/project -type f -name '*.log' -print | wc -l
12

4. Skip a subtree with prune

-prune stops find descending into a matching directory. Pair it with OR so the rest of the expression only runs on paths that were not pruned:

$ find /path/to/project -path '*/cache' -prune -o -type f -print
/path/to/project/README.md
/path/to/project/src/main.c

The left side matches the cache directory and prunes it; the right side prints ordinary files everywhere else. Unlike filtering the output afterwards, the cache contents are never visited.

Tip: If the directory name must also be excluded from another condition, add explicit grouping and test on a small tree first.

5. Handle awkward filenames

Newlines, spaces, quotes and wildcard characters are all valid in Unix filenames. Line-by-line output is for human eyes, not a safe way to pass data on. Use -print0 with a consumer that understands NUL separators:

$ find /path/to/project -type f -name '*.log' -print0 | xargs -0 -- printf '%s\n'
/path/to/project/app.log
/path/to/project/logs/error report.log

To run a command on matches, prefer -execdir over the inherently insecure -exec form, where your environment has it:

$ find /path/to/project -type f -name '*.c' -execdir wc -l '{}' \;
8 ./main.c

Warning: -execdir still runs a command. Review the command and its arguments before pointing it at untrusted directories.

6. Treat deletion as an explicit, destructive step

Warning: -delete removes matching files and directories. It is not a move to a recycle bin and cannot be undone. Never test it first on a valuable tree, and never pair it with an accidentally broad starting point.

First swap the action for -print and inspect the complete result:

$ find /path/to/project -type f -name '*.tmp' -print
/path/to/project/build/output.tmp

Only after that review, run a tightly scoped delete. -delete implies depth-first traversal, so directory contents go before their parent directories:

$ find /path/to/project/tmp -type f -name '*.tmp' -delete
$ find /path/to/project/tmp -type f -name '*.tmp' -print

Checkpoint: The second command prints nothing if every matching file was removed.

Recovery: If the pattern was wrong, restore from your backup or snapshot. In current findutils releases a failed -delete gives a non-zero exit status, but find may carry on with other paths, so check the status and any diagnostics.

7. Diagnose failures and stop early

Exit status zero means every file was processed successfully. Non-zero means errors occurred and the results are not complete. Capture it immediately in a script:

$ find /path/to/project -type f -print > /tmp/project-files.txt
$ status=$?
$ printf 'find status: %s\n' "$status"
find status: 0
$ find /path/to/project -type f -name 'README.md' -print -quit
/path/to/project/README.md

Done means