Finding things
find
Walk a directory tree and print, or act on, every file that matches a set of tests.
find [STARTING-POINT...] [EXPRESSION]
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'
./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*'
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
. ./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'
./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 {} +
-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
./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
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' \)
./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
./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 {} +
./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 {} \;
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
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.bakfind . -name '*.bak'
find . -name '*.bak' -delete
find . -name '*.bak' | wc -l
./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
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'
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 *.txtworks only until the current directory contains a.txtfile, then the shell expands it and find gets the wrong arguments. Always quote the pattern.-deleteis an action evaluated in place: anything before it filters, anything after it is too late. It also turns on-depth, which makes-pruneineffective, so do not combine them.find ... | xargs rmbreaks on names with spaces, quotes or newlines. Use-exec rm {} +or-print0 | xargs -0 rm.-owithout parentheses binds loosely:find . -name '*.c' -o -name '*.h' -exec ls {} +only runslson the.hfiles. Wrap the alternatives in\( ... \).-size -1Mmatches only empty files, because sizes round up to whole megabytes. Use-size -1024kor 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 xfails there; writefind . -name x.-printf,-regextype, and the-readable/-writable/-executabletests are GNU additions.-printfin particular does not exist on macOS; use-exec stat -f ... {} +there.-maxdepth,-mindepth,-deleteand-print0are 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.
| Option | What it does |
|---|---|
-name PATTERN | Base name matches the shell glob PATTERN. |
-iname PATTERN | Like -name, ignoring case. |
-path PATTERN | Whole path matches the glob PATTERN (/ is not special). |
-regex PATTERN | Whole path matches the regular expression PATTERN. |
-type TYPE | File type: f file, d directory, l symlink, and others. |
-size N | Size compared with N, e.g. +10M (more), -1k (less); units k M G. |
-empty | Empty file or empty directory. |
-mtime N | Modified N 24-hour periods ago; +N more, -N fewer. |
-mmin N | Modified N minutes ago; +N more, -N fewer. |
-newer FILE | Modified more recently than FILE. |
-user NAME | Owned by user NAME. |
-perm MODE | Permission bits match MODE exactly, or with -MODE / /MODE, all or any. |
-maxdepth N | Descend at most N levels below the starting points. |
-mindepth N | Apply tests only at N levels or deeper. |
-prune | Do not descend into the current directory. |
-not | Negate the following test; same as !. |
-o | OR the expressions on either side. |
-a | AND the expressions on either side (the default). |
-print | Print the path followed by a newline. |
-print0 | Print the path followed by a NUL byte, for xargs -0. |
-printf FORMAT | Print 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. |
-delete | Delete the matching files and empty directories. |
-L | Follow symbolic links while walking. |
-xdev | Stay on the starting point's filesystem. |
-depth | Process a directory's contents before the directory itself. |
-ls | List each file in ls -dils format. |
Related
xargsRead items from standard input and run a command with them as arguments, in batches or in parallel.grepPrint the lines of files (or standard input) that match a pattern.lsList the contents of directories, or the details of individual files.duShow how much disk space files and directories use, totalled per directory.statShow a file's metadata (size, permissions, owner, inode, timestamps), or print chosen fields in your own format.treeDraw a directory and everything below it as an indented tree.rmDelete files, and with -r whole directory trees. There is no undo.- GlobbingHow *, ? and [...] match file names, what happens when nothing matches, and the shell options nullglob, failglob, dotglob, globstar and extglob.
- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.