find

Locate files by name, type, size, age, and more

Updated 2026-07-05

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.

Tests and actions, combined with 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

find's expression is evaluated left to right with short-circuit logic, same as &&/|| in a shell. The order affects both correctness and speed: 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 decides whether -prune works at all. 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). The same idiom with ! -readable in front of it is how you search a tree without the permission denied errors, which is a better answer than 2>/dev/null for reasons of exit status as much as of noise.

-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 {} \; runs cmd once 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 when cmd's argument order doesn't matter.
  • -delete removes 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 ... +. Use it when you need xargs-specific features like -P (parallelism).

See ls for reading the tree find walks, including the -lAR listing that this page's own sample-file block is captured with.

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. Bulk rename a batch of files is that pairing written out in full, for a job where getting it wrong renames the wrong thing.

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.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

projects/ the tree every example below searches - ls -lAR projects

projects:
total 20
drwxr-xr-x 2 user user 4096 Jun 26 10:00 .hidden
drwxr-xr-x 2 user user 4096 Jun 26 10:00 backups
drwxr-xr-x 2 user user 4096 Jun 26 10:00 empty-dir
drwxr-xr-x 2 user user 4096 Jun 26 10:00 logs
drwxr-xr-x 3 user user 4096 Jun 26 10:00 src

projects/.hidden:
total 4
-rw-r--r-- 1 user user 13 Jun 24 10:00 .env

projects/backups:
total 16
-rw------- 1 user user 5120 May 31  2025 site-2026-01-01.tar.gz
-rw-r--r-- 1 user user 5120 Jun 15 15:30 site-2026-06-01.tar.gz

projects/empty-dir:
total 0

projects/logs:
total 6156
-rw-r--r-- 1 user user   10240 Jun 21 10:00 app.log
-rw-r--r-- 1 user user 6291456 Jun 20 10:00 big.log
lrwxrwxrwx 1 user user      14 Jun 25 10:00 main-link.js -> ../src/main.js
lrwxrwxrwx 1 user user      12 Jun 25 10:00 rotated-link -> app.log.1.gz

projects/src:
total 12
-rwxrwxrwx 1 user user    8 Jun 23 10:00 main.js
-rw-r--r-- 1 user user    8 Jun  1 09:00 util.js
drwxr-xr-x 2 user user 4096 Jun 26 10:00 vendor

projects/src/vendor:
total 4
-rw-r--r-- 1 user user 7 Jun 22 10:00 lib.js
27 outputs, collapsed by default

Matching by name

The most common find, by a mile. Note the | sort on examples that match more than one file: find prints in directory order, which is whatever order the filesystem hands back, so sorting is what makes a result you can compare against.

Find files by extension

find projects -name "*.js" | sort

-name matches against the filename only (not the full path), and is case-sensitive.

Show output
projects/logs/main-link.js
projects/src/main.js
projects/src/util.js
projects/src/vendor/lib.js

Match a name case-insensitively

find projects -iname "*.LOG" | sort

-iname is the case-insensitive version of -name, matching app.log, App.Log, APP.LOG.

Show output
projects/logs/app.log
projects/logs/big.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
projects/.hidden/.env

Match against the full path, not just the filename

find projects -path "*/vendor/*" | sort

-path matches the whole path so far, useful for excluding or targeting a specific subdirectory by name anywhere in the tree.

Show output
projects/src/vendor/lib.js

Match a name using extended regex

find projects -regex ".*\.\(js\|log\)$" | sort

-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
projects/logs/app.log
projects/logs/big.log
projects/logs/main-link.js
projects/src/main.js
projects/src/util.js
projects/src/vendor/lib.js

Filtering by type

Files, directories, symlinks: find sees them all differently.

Find only regular files

find projects -type f | sort

-type f excludes directories, symlinks, and other special files, usually what you want before acting on results.

Show output
projects/.hidden/.env
projects/backups/site-2026-01-01.tar.gz
projects/backups/site-2026-06-01.tar.gz
projects/logs/app.log
projects/logs/big.log
projects/src/main.js
projects/src/util.js
projects/src/vendor/lib.js

Find only directories

find projects -type d | sort

Includes the starting directory itself (projects) unless you filter it out, which trips people up when piping straight into a delete.

Show output
projects
projects/.hidden
projects/backups
projects/empty-dir
projects/logs
projects/src
projects/src/vendor

Find symlinks

find projects -type l | sort

-type l matches the symlink itself, not whatever it points to. ln has the counterpart, -xtype l, for the broken ones.

Show output
projects/logs/main-link.js
projects/logs/rotated-link

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
projects/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
projects/logs/rotated-link

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
projects/logs/big.log

Find files within a size range

find projects -size +1M -size -10M

The -size tests combine with the implicit AND, giving you a range.

Show output
projects/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 projects/logs/big.log
10240 projects/logs/app.log
5120 projects/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 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 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
projects/backups/site-2026-01-01.tar.gz

Find files newer than a reference file

find projects -newer projects/src/util.js -type f | sort

-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. touch -d puts that marker wherever you need it.

Show output
projects/.hidden/.env
projects/backups/site-2026-06-01.tar.gz
projects/logs/app.log
projects/logs/big.log
projects/src/main.js
projects/src/vendor/lib.js

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 | sort

-maxdepth 1 stops find from descending past the starting directory's immediate children.

Show output
projects
projects/.hidden
projects/backups
projects/empty-dir
projects/logs
projects/src

Skip the top levels and only match deeper files

find projects -mindepth 2 -type f | sort

-mindepth is the inverse of -maxdepth, excluding files that are too shallow instead of too deep.

Show output
projects/.hidden/.env
projects/backups/site-2026-01-01.tar.gz
projects/backups/site-2026-06-01.tar.gz
projects/logs/app.log
projects/logs/big.log
projects/src/main.js
projects/src/util.js
projects/src/vendor/lib.js

Skip an entire subdirectory

find projects -type d -name vendor -prune -o -type f -print | sort

-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
projects/.hidden/.env
projects/backups/site-2026-01-01.tar.gz
projects/backups/site-2026-06-01.tar.gz
projects/logs/app.log
projects/logs/big.log
projects/src/main.js
projects/src/util.js

Stay on one filesystem

sudo find / -xdev -name "*.log" -size +100M

-xdev stops find descending into anything mounted elsewhere, so a search of / doesn't wander into /proc, a network share or an external disk. Essential when searching from the root of a machine with mounts you don't want to walk, and the reason a whole-system search can otherwise take minutes.

Find files with an exact permission mode

find projects -perm 777 | sort

-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
projects/logs/main-link.js
projects/logs/rotated-link
projects/src/main.js

Acting on matches

-exec, -delete, and piping to other commands.

Run a command on each match

find projects -name "*.tar.gz" -exec ls -lh {} \; | sort

{} 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------- 1 user user 5.0K May 31  2025 projects/backups/site-2026-01-01.tar.gz
-rw-r--r-- 1 user user 5.0K Jun 15 15:30 projects/backups/site-2026-06-01.tar.gz

Batch multiple matches into fewer command invocations

find projects -type f -exec md5sum {} + | sort -k2

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
370410133629531d6f86547621ad819d  projects/.hidden/.env
4f3f1f5fda178eee12ed001a18a61d9e  projects/backups/site-2026-01-01.tar.gz
27b76a39842f71707d6028436b0ba068  projects/backups/site-2026-06-01.tar.gz
a3f149852edbdb4a491db2749263510b  projects/logs/app.log
fcebc6752f074048b314629e78a9765a  projects/logs/big.log
d4c73f111c0ef6d8782c0d6f9ef2bca4  projects/src/main.js
fc50f2bdf153bce5a7b24c2dabf14bb2  projects/src/util.js
73441ca386234c947b81650654869102  projects/src/vendor/lib.js

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 | sort

For a single exclusion, -not -path is shorter than the -prune idiom. Use -prune instead when you also need find to skip descending into a large excluded directory for performance.

Show output
projects/.hidden/.env
projects/backups/site-2026-01-01.tar.gz
projects/backups/site-2026-06-01.tar.gz
projects/logs/app.log
projects/logs/big.log
projects/src/main.js
projects/src/util.js

Match either of two patterns

find projects \( -name "*.js" -o -name "*.log" \) | sort

-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
projects/logs/app.log
projects/logs/big.log
projects/logs/main-link.js
projects/src/main.js
projects/src/util.js
projects/src/vendor/lib.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
projects/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.

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 projects/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