find
Locate files by name, type, size, age, and more
find walks a directory tree and prints, or acts on, every file that matches a set of tests
you give it. Where locate searches a pre-built index of filenames (fast, but can be stale and
only matches names), find inspects the filesystem live: name, type, size, age, permissions,
ownership, all of it, right now.
The mental model: tests, actions, and an implicit AND
A find command is a directory to start from, followed by a chain of tests and
actions:
find ~/projects -name "*.log" -size +10M
Each test (-name, -size, -mtime, …) either matches a given file or it doesn't. Chain
several and find combines them with an implicit AND: a file has to pass every test to be
included. Use -o for OR, and parentheses (escaped as \( \) in most shells) to group:
find . \( -name "*.js" -o -name "*.log" \) -not -path "*/vendor/*"
By default the only "action" is -print (implied if you don't specify one), but -exec,
-delete, and -printf let you act on or format each match directly, without piping to another
command.
Tests evaluate left to right, and that's a feature
find's expression is evaluated left to right with short-circuit logic, same as &&/|| in
a shell. This matters for both correctness and performance: put your cheapest, most-selective
test first (usually -name or -type) so expensive tests like -exec only run against
candidates that already passed the cheap filters. It also matters for -prune. To skip a whole
directory, -prune has to come before whatever test would otherwise print or recurse into it
(see the "skip a directory" example).
-exec vs -delete vs piping to xargs
Three ways to act on matches, in order of how much you should trust yourself with them:
-exec cmd {} \;runscmdonce per matched file. Safe and simple, but slow on huge result sets since it forks once per file.-exec cmd {} +batches as many matches as fit on one command line into fewer invocations. Faster, but only works whencmd's argument order doesn't matter.-deleteremoves matches directly, no subprocess at all. The fastest option and the most dangerous, because there's no confirmation step.- Piping to
xargs(find ... -print0 | xargs -0 cmd) is the classic alternative to-exec ... +. Reach for it when you needxargs-specific features like-P(parallelism).
Filenames with spaces and newlines
Piping find's default newline-separated output into a loop or xargs breaks the moment a
filename contains a space or newline. -print0 separates matches with a NUL byte instead, which
can't appear in a filename; pair it with xargs -0 or while IFS= read -r -d '' f. This isn't
a hypothetical edge case. It bites the first time your script meets a file like Meeting Notes (final).docx.
Permission tests are exact-match by default
-perm 644 matches files whose mode is exactly 644, not "at least these bits." To test
"this bit is set, I don't care about the rest," prefix with -: -perm -u+x matches anything
the owner can execute, regardless of group/other bits. This trips people up constantly; if your
-perm test is matching nothing, check whether you meant the exact form or the --prefixed
"at least" form.
Matching by name
The most common find, by a mile.
Find files by extension
find ~/projects -name "*.js"
`-name` matches against the filename only (not the full path), and is case-sensitive.
Show output
./logs/main-link.js
./src/util.js
./src/vendor/lib.js
./src/main.js
Match a name case-insensitively
find ~/projects -iname "*.LOG"
`-iname` is the case-insensitive version of `-name`, matching `app.log`, `App.Log`, `APP.LOG`.
Show output
./logs/big.log
./logs/app.log
Match hidden (dotfiles) files
find ~/projects -name ".*" -type f
A leading `.` isn't special to `find` the way it is to the shell's globbing. `-name ".*"` matches dotfiles like a normal pattern.
Show output
./.hidden/.env
Match against the full path, not just the filename
find ~/projects -path "*/vendor/*"
`-path` matches the whole path so far, useful for excluding or targeting a specific subdirectory by name anywhere in the tree.
Show output
./src/vendor
./src/vendor/lib.js
Match a name using extended regex
find ~/projects -regex ".*\.\(js\|log\)$"
`-regex` matches the entire path against a regex (BRE by default; add `-regextype posix-extended` for ERE syntax). More powerful than `-name` when you need alternation or anchors.
Show output
./logs/main-link.js
./logs/big.log
./logs/app.log
./src/util.js
./src/vendor/lib.js
./src/main.js
Filtering by type
Files, directories, symlinks: find sees them all differently.
Find only regular files
find ~/projects -type f
`-type f` excludes directories, symlinks, and other special files, usually what you want before acting on results.
Show output
./backups/site-2026-06-01.tar.gz
./backups/site-2026-01-01.tar.gz
./logs/big.log
./logs/app.log
./src/util.js
./src/vendor/lib.js
./src/main.js
./.hidden/.env
Find only directories
find ~/projects -type d
Includes the starting directory itself (`.`) unless you filter it out.
Show output
.
./backups
./logs
./src
./src/vendor
./.hidden
./empty-dir
Find symlinks
find ~/projects -type l
`-type l` matches the symlink itself, not whatever it points to.
Show output
./logs/main-link.js
Find symlinks that point to a regular file
find ~/projects -type l -xtype f
`-xtype` follows the link before testing type. This finds symlinks whose *target* is a regular file, as opposed to a broken link or a link to a directory.
Show output
./logs/main-link.js
Find broken symlinks
find ~/projects -xtype l
When a symlink's target has been moved or deleted, resolving its type fails and falls back to reporting `l`. This is the standard "find broken links" idiom.
Show output
./logs/main-link.js
Filtering by size
Track down what's actually using disk space.
Find files larger than a given size
find ~/projects -size +1M
`+1M` means strictly greater than 1 megabyte. Suffixes: `c` bytes, `k` kilobytes, `M` megabytes, `G` gigabytes.
Show output
./logs/big.log
Find files smaller than a given size
find ~/projects -size -1k
`-1k` means strictly less than 1 kilobyte, useful for finding suspiciously empty-looking files.
Find files within a size range
find ~/projects -size +1M -size -10M
Two `-size` tests combine with the implicit AND, giving you a range.
Show output
./logs/big.log
Find and list the biggest files, largest first
find ~/projects -type f -printf "%s %p\n" | sort -rn | head -3
`-printf "%s %p\n"` prints size in bytes then path; piping through `sort -rn` ranks by size numerically, largest first.
Show output
6291456 ./logs/big.log
10240 ./logs/app.log
5120 ./backups/site-2026-06-01.tar.gz
Filtering by age
mtime, mmin, and finding what changed recently (or didn't).
Find files modified in the last 24 hours
find ~/projects -mtime -1
`-mtime -1` means modified less than 1 day ago. The sign convention is the same as `-size`: `-N` is less than, `+N` is more than, `N` is exactly.
Find files modified in the last 90 minutes
find ~/projects -mmin -90
`-mmin` is the minutes version of `-mtime`, for finer-grained recency than a full day.
Find files accessed recently, not just modified
find ~/projects -type f -amin -120
`-amin`/`-atime` test access time instead of modification time, useful for finding files nothing has *read* in a while, as opposed to files nobody has *written* to. Many systems mount filesystems with `noatime` for performance, which makes this test a no-op there.
Find files modified today, by the calendar, not the last 24 hours
find ~/projects -daystart -mtime 0
`-mtime 0` alone means "less than 24 hours ago," which drifts with what time you run the command. `-daystart` measures from the start of today instead, so "0 days" means "today's calendar date."
Find files not modified in over a year
find ~/projects -type f -mtime +365
A common first step before archiving or deleting old backups. See the danger note below before adding `-delete`.
Show output
./backups/site-2026-01-01.tar.gz
Find files newer than a reference file
find ~/projects -newer src/util.js -type f
`-newer` compares modification times against another file instead of the current time, useful for "what changed since the last deploy", using a marker file's timestamp as the cutoff.
Show output
./backups/site-2026-06-01.tar.gz
./logs/big.log
./logs/app.log
./src/vendor/lib.js
./src/main.js
./.hidden/.env
Depth and pruning
Controlling how far find descends, and skipping subtrees entirely.
Limit the search to the top level only
find ~/projects -maxdepth 1 -type d
`-maxdepth 1` stops find from descending past the starting directory's immediate children.
Show output
.
./backups
./logs
./src
./.hidden
./empty-dir
Skip the top levels and only match deeper files
find ~/projects -mindepth 2 -type f
`-mindepth` is the inverse of `-maxdepth`, excluding files that are too shallow instead of too deep.
Show output
./backups/site-2026-06-01.tar.gz
./backups/site-2026-01-01.tar.gz
./logs/big.log
./logs/app.log
./src/util.js
./src/vendor/lib.js
./src/main.js
./.hidden/.env
Skip an entire subdirectory
find ~/projects -type d -name vendor -prune -o -type f -print
`-prune` has to come before the `-o` so it applies to the vendor match specifically; without it, `find` would still descend into `vendor/` looking for files. This idiom (`-name X -prune -o ... -print`) is the standard way to exclude a directory like `node_modules` or `.git`.
Show output
./backups/site-2026-06-01.tar.gz
./backups/site-2026-01-01.tar.gz
./logs/big.log
./logs/app.log
./src/util.js
./src/main.js
./.hidden/.env
Stay on one filesystem
find / -xdev -maxdepth 1 -type d
`-xdev` (a.k.a. `-mount`) prevents find from crossing into mounted filesystems (network shares, other drives, `/proc`) when searching from a high-level directory like `/`.
Show output
/
/tmp
/boot
Permissions and ownership
Exact-match vs at-least-these-bits: see the note in the intro.
Find files with an exact permission mode
find ~/projects -perm 777
`-perm 777` (no prefix) matches only files whose mode is *exactly* `777`. Note this also matches symlinks, whose own permissions are always shown as `777` regardless of the target's real permissions.
Show output
./logs/main-link.js
./src/main.js
Find files where the owner can execute, regardless of other bits
find ~/projects -perm -u+x
The `-` prefix switches to "at least these bits set" matching. This finds every directory (always executable/traversable) plus any file the owner can run.
Find files owned by the current user
find ~/projects -user "$(whoami)"
`-user` filters by owner name (or numeric UID).
Acting on matches
-exec, -delete, and piping to other commands.
Run a command on each match
find ~/projects -name "*.tar.gz" -exec ls -lh {} \;
`{}` is replaced with the matched path, and `\;` (escaped so the shell doesn't eat the semicolon) ends the `-exec` expression. Runs `ls -lh` once per file.
Show output
-rw-r--r-- 1 user user 5.0K Jun 15 15:30 ./backups/site-2026-06-01.tar.gz
-rw------- 1 user user 5.0K May 31 2025 ./backups/site-2026-01-01.tar.gz
Batch multiple matches into fewer command invocations
find ~/projects -type f -exec md5sum {} +
Ending with `+` instead of `\;` passes as many matched files as fit on one command line to a single `md5sum` invocation, instead of spawning one process per file. Much faster over large result sets.
Show output
./backups/site-2026-06-01.tar.gz
./backups/site-2026-01-01.tar.gz
./logs/big.log
./logs/app.log
./src/util.js
./src/vendor/lib.js
./src/main.js
./.hidden/.env
Delete matches directly
find ~/projects -type f -name "*.tar.gz" -mtime +365 -delete
`-delete` removes matches with no subprocess and no confirmation. Always run the identical command with `-print` (or no action) first and read the list before adding `-delete`.
Remove empty directories
find ~/projects -type d -empty -delete
Combine `-type d` and `-empty` to target only directories with nothing in them.
Feed matches to xargs for parallel processing
find ~/projects -type f -name "*.log" -print0 | xargs -0 -P 4 gzip
`-print0` / `xargs -0` handles filenames with spaces safely (see the note in the intro); `xargs -P 4` runs up to 4 `gzip` processes in parallel.
Combining tests with logic
AND is implicit; OR and NOT need to be spelled out.
Negate a test
find ~/projects -not -name "*.js"
`-not` (or `!`, escaped as `\!` in most shells) inverts the following test.
Exclude an entire path pattern without -prune
find ~/projects -not -path "*/vendor/*" -type f
For a single exclusion, `-not -path` is shorter than the `-prune` idiom. Reach for `-prune` instead when you also need find to skip *descending into* a large excluded directory for performance.
Show output
./backups/site-2026-06-01.tar.gz
./backups/site-2026-01-01.tar.gz
./logs/big.log
./logs/app.log
./src/util.js
./.hidden/.env
Match either of two patterns
find ~/projects \( -name "*.js" -o -name "*.log" \)
`-o` is OR; the parentheses are required here because `find`'s default AND would otherwise bind more tightly than you want. Escape them (`\(` `\)`) so the shell passes them through literally.
Show output
./logs/main-link.js
./logs/big.log
./logs/app.log
./src/util.js
./src/vendor/lib.js
./src/main.js
Combine a name filter with an age filter
find ~/projects -type f -name "*.tar.gz" -mtime +365
Multiple tests chained with no explicit operator are ANDed together. This narrows straight to old, matching backups.
Show output
./backups/site-2026-01-01.tar.gz
Practical one-liners
Common real-world tasks that combine several of the tests above.
Count files by type
find ~/projects -type f | wc -l
A quick sanity check before a bulk operation: how many files am I actually about to touch?
Show output
8
Find and grep in one pipeline
find ~/projects -name "*.js" -print0 | xargs -0 grep -l "TODO"
Narrow the file set with `find` first, then hand it to `grep`. Faster than `grep -r` when you need find's richer filtering (age, size, permissions) as well as a pattern match. See [grep](/commands/grep/).
Find the single largest file under a path
find ~/projects -type f -printf "%s %p\n" | sort -rn | head -1
The one-result version of the size-ranking example above, handy inline in a script.
Show output
6291456 ./logs/big.log
Count matches across two patterns
find ~/projects \( -name "*.log" -o -name "*.tar.gz" \) | wc -l
Group an OR expression, then pipe the whole result through `wc -l` for a total count instead of a list.
Show output
4