opscape

Finding things

find

Walk a directory tree and print, or act on, every file that matches a set of tests.

find [STARTING-POINT...] [EXPRESSION]

15 of 15 examples run in Debian 13.7 on 2026-10-31 · findutils 4.10.0-3 · manual: find(1)

find walks one or more directory trees and evaluates an expression for every file and directory it meets. The expression is a chain of tests (-name, -type, -size, -mtime, ...) and actions (-print, -exec, -delete, ...), joined by an implicit AND and evaluated left to right; as soon as one test is false, find moves on to the next file. If you give no action, -print is assumed.

Its options are whole words with a single dash (-name, not --name), and their order matters: -maxdepth belongs right after the starting point, and -delete placed before a test deletes everything. Patterns for -name are shell globs, not regexes, and must be quoted so the shell does not expand them first.

To run a command on the results, use -exec cmd {} + (many files per command) or pipe -print0 into xargs -0; both handle file names with spaces and newlines. If you only want to search file contents, grep -r is shorter; find is the tool when the selection depends on names, types, sizes, times or permissions.

Examples

Find files by name

find . -name '*.py'
explain
output
./src/lib/helpers.py
./src/app.py

-name matches the file's base name against a glob. The quotes matter: unquoted, the shell would expand *.py against the current directory before find ran. Results come in directory order, not sorted.

Match names regardless of case

find photos -iname 'img*'
explain
output
photos/IMG_2041.png

-iname is the case-insensitive -name, so img* matches IMG_2041.png. The starting point photos limits the search to that directory.

List only directories, or only files

find . -type d
echo ---
find . -type l
output
.
./logs
./src
./src/lib
./data
./scripts
./photos
---
./link-to-notes

-type d selects directories (including the starting point .), -type f regular files, -type l symbolic links. Here the only symlink is link-to-notes.

Limit how deep find goes

find . -maxdepth 1 -type f -name '*.txt'
explain
output
./file with spaces.txt
./todo.txt
./notes.txt

-maxdepth 1 looks only at the starting directory's direct entries, so data/fruits.txt and the other nested .txt files are skipped. Put it before the tests; GNU find warns if it comes after them.

Find files by size

find . -type f -size +1k -exec ls -l {} +
explain
output
-rw-rw-r-- 1 dev dev 2309 Oct 24 21:55 ./access.log
-rw-rw-r-- 1 dev dev 1587 Oct 27 18:41 ./photos/beach.png
-rw-rw-r-- 1 dev dev 2315 Oct 28 05:43 ./photos/forest.png

-size +1k means larger than 1 KiB. Sizes are rounded up to whole units before comparing, which surprises people: -size -1k only matches empty files. M and G work for bigger units. -exec ls -l {} + shows the sizes.

Find recently modified files

find . -type f -mtime -4 | sort
explain
output
./photos/forest.png
./scripts/backup.sh
./src/Makefile
./src/app.py
./src/lib/helpers.py
./src/main.c
./src/util.sh

-mtime -4 matches files modified less than four 24-hour periods ago; +4 means more than that, and plain 4 means "between 4 and 5 days". The sandbox's fixture files are aged between two and seven days, so this lists the newer part of the tree. Use -mmin for minutes.

Find files changed after a reference file

find src -newer src/Makefile
explain
output
src/util.sh
src/app.py
src/lib
src/lib/helpers.py
src/main.c

-newer FILE selects anything modified more recently than FILE. It is the reliable way to ask "what changed since the last build", by touching a stamp file after each run.

Combine tests with OR and parentheses

find . -type f \( -name '*.sh' -o -name '*.py' \)
explain
output
./scripts/backup.sh
./scripts/greet.sh
./src/app.py
./src/util.sh
./src/lib/helpers.py

-o is OR. Tests are ANDed by default and AND binds tighter than OR, so without the parentheses -type f would only apply to the *.sh side. The parentheses are escaped because they are special to the shell. To exclude instead, negate a test with ! or -not: find data -type f ! -name '*.json'.

Skip a directory entirely with -prune

find . -path ./data -prune -o -type f -name '*.txt' -print
explain
output
./todo.txt
./notes.txt
./file with spaces.txt

When the path is ./data, -prune stops find from descending into it and the -o branch is not evaluated. Every other path falls through to the right side. The explicit -print is needed: without it find would also print the pruned ./data itself.

Run a command on all results at once with -exec {} +

find . -name '*.py' -exec grep -n TODO {} +
explain
output
./src/app.py:11:    # TODO: read the port from the environment
./src/lib/helpers.py:2:    # TODO: handle unicode

With +, find replaces {} with as many file names as fit on one command line and runs grep once, so grep prefixes each line with its file name. This is the fast form.

Run a command once per file with -exec {} \;

find . -name '*.py' -exec grep -n TODO {} \;
explain
output
11:    # TODO: read the port from the environment
2:    # TODO: handle unicode

With \; grep runs once per file and gets a single name each time, so it does not print file names, which makes the output less useful here. Use \; only when the command takes exactly one file, as mv {} {}.old does. The ; is escaped so the shell passes it to find.

Pipe names safely to xargs with -print0

find . -maxdepth 1 -name '*.txt' -print0 | xargs -0 wc -l
explain
output
  6 ./notes.txt
  1 ./file with spaces.txt
  5 ./todo.txt
 12 total

-print0 ends each name with a NUL byte instead of a newline and xargs -0 splits on NUL, so file with spaces.txt arrives as one argument. Plain find | xargs would split it into three words and wc would fail on each.

Delete matching files, checking first

Setup run before this example
touch data/old1.bak data/old2.bak src/main.c.bak
find . -name '*.bak'
find . -name '*.bak' -delete
find . -name '*.bak' | wc -l
output
./data/old1.bak
./data/old2.bak
./src/main.c.bak
0

Always run the same expression without -delete first and read the list. -delete must come last: find . -delete -name '*.bak' deletes every file before the name is tested. The final count of 0 confirms they are gone.

Print custom details with -printf

find data -type f -printf '%s\t%p\n' | sort -n | tail -3
explain
output
405	data/sales.csv
436	data/users.csv
503	data/servers.json

-printf (GNU only) formats each result: %s is the size in bytes, %p the path. Sorted numerically, the last three lines are the largest files under data.

A missing starting point is an error

find nosuchdir -name '*.log'
explain
outputexit 1
find: ‘nosuchdir’: No such file or directory

find reports the problem on stderr and exits with status 1, but it still processes any other starting points it was given. Permission-denied directories cause the same status.

Common pitfalls

  • Unquoted globs: find . -name *.txt works only until the current directory contains a .txt file, then the shell expands it and find gets the wrong arguments. Always quote the pattern.

  • -delete is an action evaluated in place: anything before it filters, anything after it is too late. It also turns on -depth, which makes -prune ineffective, so do not combine them.

  • find ... | xargs rm breaks on names with spaces, quotes or newlines. Use -exec rm {} + or -print0 | xargs -0 rm.

  • -o without parentheses binds loosely: find . -name '*.c' -o -name '*.h' -exec ls {} + only runs ls on the .h files. Wrap the alternatives in \( ... \).

  • -size -1M matches only empty files, because sizes round up to whole megabytes. Use -size -1024k or a smaller unit.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • BSD/macOS find requires a starting point: find -name x fails there; write find . -name x.

  • -printf, -regextype, and the -readable/-writable/-executable tests are GNU additions. -printf in particular does not exist on macOS; use -exec stat -f ... {} + there.

  • -maxdepth, -mindepth, -delete and -print0 are available in both GNU and current BSD/macOS find, though not in POSIX.

Options worth knowing

A selection, described in our own words. For the full list, see find(1) or find --help.

OptionWhat it does
-name PATTERNBase name matches the shell glob PATTERN.
-iname PATTERNLike -name, ignoring case.
-path PATTERNWhole path matches the glob PATTERN (/ is not special).
-regex PATTERNWhole path matches the regular expression PATTERN.
-type TYPEFile type: f file, d directory, l symlink, and others.
-size NSize compared with N, e.g. +10M (more), -1k (less); units k M G.
-emptyEmpty file or empty directory.
-mtime NModified N 24-hour periods ago; +N more, -N fewer.
-mmin NModified N minutes ago; +N more, -N fewer.
-newer FILEModified more recently than FILE.
-user NAMEOwned by user NAME.
-perm MODEPermission bits match MODE exactly, or with -MODE / /MODE, all or any.
-maxdepth NDescend at most N levels below the starting points.
-mindepth NApply tests only at N levels or deeper.
-pruneDo not descend into the current directory.
-notNegate the following test; same as !.
-oOR the expressions on either side.
-aAND the expressions on either side (the default).
-printPrint the path followed by a newline.
-print0Print the path followed by a NUL byte, for xargs -0.
-printf FORMATPrint details using a format such as %p %s %TY.
-exec CMD {} ;Run CMD on each file; end with + to pass many files at once.
-execdir CMD {} ;Like -exec, but run from the file's own directory.
-ok CMD {} ;Like -exec, but ask for confirmation first.
-deleteDelete the matching files and empty directories.
-LFollow symbolic links while walking.
-xdevStay on the starting point's filesystem.
-depthProcess a directory's contents before the directory itself.
-lsList each file in ls -dils format.

Related