The searching lesson introduced find by name. In real work you rarely know the name — you know that a file is big, or old, or was changed after a deployment, or belongs to a user who has left. This lesson covers the tests that describe those situations, how to combine them with boolean logic, and how to run commands on every match safely and quickly.
find walks a directory tree and evaluates an expression against every entry. The expression is made of tests (which match or not) and actions (which do something). The default action is -print.
find <start-dirs> [options] <tests> [actions]
find . -type f -name "*.py" # from here, regular files ending in .py
find /etc /opt -maxdepth 1 -type d # two start points, one level deepAlways quote patterns like "*.py"; otherwise the shell expands the wildcard before find sees it.
| Test | Matches |
|---|---|
| -type f / -type d / -type l | regular file / directory / symlink |
| -name "*.log" | base name, case-sensitive glob |
| -iname "readme*" | base name, case-insensitive |
| -path "*/tests/*" | the whole path |
| -regex ".*/IMG_[0-9]+\.jpg" | whole path, regex (Emacs syntax by default; add -regextype posix-extended for ERE) |
| -empty | empty files or directories |
Every file carries three timestamps: modification (m, content changed), change (c, metadata such as permissions changed) and access (a, read). find exposes them in days and minutes. A number +n means "more than n", -n means "less than n", and a bare n means "exactly n".
find ~/Downloads -type f -mtime +30 # not modified in the last 30 days
find /var/log -type f -mmin -60 # modified within the last hour
find /etc -newer /etc/hostname # modified after that reference file
find . -type f -newermt "2026-09-01" # modified after a date-mtime +7 counts whole 24-hour periods, so it matches files at least 8 days old. Use -mmin when precision matters.
find / -type f -size +1G 2>/dev/null # larger than 1 GiB (k, M, G suffixes)
find . -type f -size -10k # smaller than 10 KiB
find /home -user alice -group developers # owned by alice and in group developers
find / -type f -perm -4000 2>/dev/null # setuid binaries (security audit)
find /var/www -type f -perm /o=w # world-writable files
find / -nouser 2>/dev/null # files whose owner no longer existsThe 2>/dev/null discards "Permission denied" noise when scanning system directories as a normal user.
Tests written next to each other are ANDed. Use -o for OR, ! or -not for NOT, and escaped parentheses for grouping — parentheses must be escaped or quoted because they mean something to the shell.
find . -type f \( -name "*.jpg" -o -name "*.png" \)
find . -type f ! -name "*.md"
find . -type f \( -name "*.log" -o -name "*.tmp" \) -mtime +14-prune stops find from descending into a directory, which is essential for skipping node_modules, .git or mounted volumes.
find . -path ./node_modules -prune -o -type f -name "*.js" -print
find . -type d \( -name .git -o -name node_modules \) -prune -o -type f -print
find / -xdev -type f -size +500M # do not cross into other mounted filesystemsWhen you use -prune you must supply an explicit -print on the other branch, otherwise the pruned directory itself is printed.
-exec runs a command for each match, with {} replaced by the file path. End the command with \; to run it once per file, or + to batch many files into one invocation (much faster, like xargs).
find . -name "*.orig" -exec rm {} \; # one rm per file
find . -name "*.orig" -exec rm {} + # one rm with many arguments
find . -name "*.sh" -exec chmod +x {} +
find . -type f -name "*.log" -exec gzip {} \;
find . -type f -exec grep -l "TODO" {} + # files containing TODO
find . -name "*.tmp" -ok rm {} \; # ask before each
find . -name "*.tmp" -delete # built-in, fastest
find . -type f -printf "%s %p\n" | sort -n | tail -5 # five largest files-delete must come last in the expression; placed first it deletes everything. Test any destructive command by running it with -print instead of the action first.
Piping find output to another program breaks when names contain spaces or newlines. Use -print0 with xargs -0 so entries are separated by null bytes, which cannot appear in filenames.
find . -type f -name "*.mp4" -print0 | xargs -0 du -ch | tail -1*.log, so the shell expands it to a single existing filename.-mtime -7 when you mean older than a week (that is +7).-exec ... \; on thousands of files when + would be hundreds of times faster.-delete or -exec before the tests.Which expression finds files not modified in the last 90 days?
find tests describe files by type, name, time, size, owner and permissions; adjacent tests are ANDed.+n means more than, -n less than; -mtime counts days and -mmin minutes.\( ... \), negate with !, and skip directories with -prune plus an explicit -print.-exec ... {} + batches matches into one command; -delete is built in and must come last.-print0 | xargs -0 whenever filenames might contain spaces.Next lesson: grep in Depth and Regular Expressions — search inside files with patterns instead of fixed strings.