find walks a directory tree and matches files against expressions: name, type, size, time, owner, permission, or anything you can compose. The expression language is its own micro-DSL with order-sensitive operators, NUL-safe output via -print0, and a handful of macOS BSD versus GNU divergences that catch out scripts shipped between platforms. This page is the reference I keep open while writing batch-rename, cleanup, and pre-deploy scripts.
What does the find command do?
find recursively searches a directory tree and prints the paths of files (and directories) that match an expression. It is the canonical tool for "find every file under X that matches Y" on Unix systems. Common uses: locate files by name (-name, -iname), filter by type (-type f, -type d, -type l), filter by size (-size +100M) or modification time (-mtime -7), then act on the results with -exec, -execdir, -delete, or by piping -print0 into xargs -0. The expression is parsed left to right; tests like -type f filter, while actions like -print or -delete operate on the matches. macOS ships BSD find, which differs from GNU find in regex defaults, the absence of -regextype, and slightly different -mtime rounding. On Windows, PowerShell's Get-ChildItem covers most of the same ground with different syntax.
Jump to:
- Basic search
- Filter by type
- Filter by size
- Filter by modification time
- Filter by permissions and owner
- Acting on matches: -exec, -execdir, -delete
- Safe pipelines: -print0 and xargs -0
- macOS BSD vs GNU find
- Common pitfalls
- What to do next
- FAQ
Basic search
Find every file with a given name in the current tree.
find . -name '*.log'Case-insensitive match.
find . -iname '*.log'PowerShell's -Filter is case-insensitive by default, matching Windows' filesystem behavior. On Linux/macOS, -name is case-sensitive (-iname is the insensitive variant).
Limit depth (don't recurse forever):
find . -maxdepth 2 -name '*.log'-maxdepth N is a GNU extension that BSD find also supports, despite being non-POSIX. Both macOS and Linux are happy with it.
Filter by type
| Test | Matches |
|---|---|
-type f | Regular files |
-type d | Directories |
-type l | Symbolic links |
-type s | Sockets |
-type p | Named pipes (FIFOs) |
-type b | Block devices |
-type c | Character devices |
find /var/log -type f -name '*.gz'Find empty files or empty directories:
find . -type f -emptyFilter by size
-size N accepts a unit suffix: c (bytes), k (KiB), M (MiB), G (GiB). Prefix with + for "greater than", - for "less than".
find . -type f -size +100MFind the biggest log files, sorted:
find /var/log -type f -name '*.log' -printf '%s %p\n' | sort -rn | head -20-printf is GNU only. On BSD use -exec stat with the BSD stat format flags. That difference is one of the most common scripts-fail-on-macOS surprises.
Filter by modification time
| Test | Meaning |
|---|---|
-mtime -N | Modified less than N days ago |
-mtime +N | Modified more than N days ago |
-mtime N | Modified exactly N days ago |
-mmin -N | Modified less than N minutes ago |
-newer FILE | Modified more recently than FILE |
-atime / -ctime | Access time / inode change time (same units) |
find . -type f -mtime -7Files older than 30 days (typical cleanup script):
find /tmp -type f -mtime +30 -deleteSubtle macOS difference: BSD find -mtime -N rounds the time differently than GNU. GNU rounds the file's age down to 24-hour units; BSD treats fractional days differently in some edge cases. For scripts that need exact behavior, use -newerXY (BSD) or -mmin (both) with explicit minutes.
Filter by permissions and owner
find . -type f -perm 0644Find anything world-writable (security hygiene check):
find / -xdev -type f -perm -o+w 2>/dev/nullFilter by owner:
find /var/www -user www-data -type fActing on matches: -exec, -execdir, -delete
-exec runs a command for each match. The {} placeholder is replaced with the path; the command must end with \; (escaped semicolon).
find . -type f -name '*.log' -exec grep -l 'ERROR' {} \;Replace \; with + to batch arguments into fewer command invocations (much faster):
find . -type f -name '*.log' -exec grep -l 'ERROR' {} +-execdir runs the command in the directory of the matched file. Safer when paths have spaces or weird characters because the {} is just the basename:
find . -type f -name '*.tmp' -execdir rm {} \;-delete is the shortest form. Use with extreme care: there is no confirmation, no dry run, and order matters (always test with -print first):
find /tmp/cache -type f -name '*.bak' -deleteA safe pattern for any destructive find: write it with -print first, eyeball the list, then change -print to -delete.
Safe pipelines: -print0 and xargs -0
find ... | xargs command looks innocent but breaks the moment a filename contains a space, newline, or quote. The fix is NUL-delimited output: -print0 from find, -0 on xargs.
find . -type f -name '*.log' -print0 | xargs -0 gzipWhy this matters: filenames on Unix can contain literally any byte except / and NUL. -print separates with newlines, but newlines are valid in filenames. The only safe separator is NUL (\0).
This pattern is the safe equivalent of find ... -exec cmd {} +, useful when you need shell features (piping, redirection) that -exec cannot give you. It pairs with the grep cheat sheet for the most common find-then-grep pipelines.
macOS BSD vs GNU find
The divergences that matter, in order of how often they bite.
| Feature | GNU find | BSD find (macOS default) |
|---|---|---|
-regextype | Supported (-regextype posix-extended, etc.) | Not supported |
-regex default flavor | POSIX BRE (Emacs flavor) | Basic POSIX |
-printf | Supported (rich format strings) | Not supported; use -exec stat |
-mtime fractional rounding | Floors to 24h units | Different rounding edge case |
-iregex | Supported | Supported, but different default flavor |
-not operator | Supported | Supported |
-delete order | Same | Same |
GNU long options (--help, etc.) | Supported | Not supported on most options |
The single most common cross-platform script failure is -printf on macOS. There is no clean BSD equivalent; either guard with if command -v gfind, or install GNU find via Homebrew (brew install findutils) and use gfind explicitly.
brew install findutils
alias find='gfind'
alias xargs='gxargs'If your shell scripts need to run on both Linux and macOS, stick to the POSIX-ish intersection: -name, -iname, -type, -size, -mtime, -mmin, -perm, -newer, -print, -print0, -exec, -execdir, -delete. Everything outside that set risks BSD/GNU drift.
Common pitfalls
1. Forgetting to quote the pattern. find . -name *.log lets the shell expand *.log first. If a matching file exists in the current directory, that gets passed as the pattern; if none does, you get a confusing "paths must precede expression" error. Always quote: find . -name '*.log'.
2. -print is unsafe in pipelines. Filenames with spaces or newlines break xargs by default. Use -print0 | xargs -0 for any pipeline that handles arbitrary paths.
3. The -exec ... {} \; semicolon is shell metacharacter. Always escape with backslash or quote: \; or ';'. Without it, the shell ends the find command at the semicolon.
4. -exec ... {} + versus -exec ... {} \;. The + form batches arguments into a single command invocation (fast). The \; form forks once per match (slow on large trees). Use + unless the command can only accept one path at a time.
5. -delete happens depth-first but order can surprise you. If you want to delete files and then empty directories, find . -depth -type d -empty -delete after the file delete pass. The -depth flag forces children before parents.
6. -maxdepth and -mindepth must come BEFORE other tests. Putting them after -name or -type is technically legal but evaluated left to right, which can produce surprising results. Place depth restrictions first.
7. -name matches the basename only, not the path. find . -name 'src/*.ts' will never match because the slash makes it a path, not a basename. Use -path '*/src/*.ts' instead.
8. find / with no filters scans the whole filesystem. Including network mounts, /proc, /sys. Always scope with a real start path or add -xdev to stay on one filesystem.
What to do next
- grep cheat sheet — the classic pairing for find.
find ... -print0 | xargs -0 grepis one of the most common pipelines. - SSH cheat sheet — for running find against remote hosts.
- curl cheat sheet — for downloading lists of files that find generates.
- Bash for loop — when you need to loop over find output with logic that does not fit in
-exec. - Bash while loop — the safe
while IFS= read -r -d '' file; do ...; done < <(find ... -print0)pattern for processing files line by line. - How to zip multiple directories into individual files — a worked example of
find ... -print0 | xargs -0 zip. - How to increase Google Cloud VM disk size without rebooting — uses
findfor the cleanup pass before resizing. - External: GNU findutils manual, FreeBSD find(1), PowerShell Get-ChildItem.



![Bash conditionals reference: if/elif/else syntax, [ ] vs [[ ]] vs (( )) test contexts, numeric, string, and file operators, the case statement, and the unquoted-variable pitfall.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=80/https://images.techearl.com/bash-if-else/bash-if-else.jpg?v=2026-01-08T11%3A24%3A00Z)

