The find command: a practical guide with examples
find walks a directory tree and prints every path that passes a set of tests: find /var/log -name '*.log' -mtime +7 lists log files last modified more than 7 full days ago. This guide covers the tests you will use most, the rounding traps in size and age, how to act on results safely, and how to delete old backups without accidents.
How a find command is built
find /var/www -type f -name '*.php' -print- Starting points (
/var/www): one or more directories to search. With none, GNU find searches the current directory. - Tests (
-type f -name '*.php'): conditions joined by an implied AND and checked left to right. - Action (
-print): what to do with each match.-printis the default, so you can leave it out.
Always quote patterns. Unquoted, the shell expands *.php against the current directory before find sees it. With two .php files in the current directory, find fails like this:
find: paths must precede expression: `b.php'
find: possible unquoted pattern after predicate `-name'?With exactly one match it is worse: find quietly searches for that single file name. Searching as a normal user, add 2>/dev/null to hide Permission denied messages, or run find with sudo.
Find by name and type
| Command | Finds |
|---|---|
find /var/www -name '*.php' | Names ending in .php. Case-sensitive; *, ? and [...] work as in the shell. |
find /var/www -iname '*.php' | The same, ignoring case, so Config.PHP matches too. |
find /var/www -type d -name uploads | Directories named uploads. |
find /var/www -path '*/uploads/*.jpg' | Matches the pattern against the whole path, not just the name. |
find /etc -type l | Symbolic links. |
-type takes f (regular file), d (directory) and l (symbolic link), plus p, s, b and c for pipes, sockets and devices. Add -type f when you mean files: in our test -name '*.php' alone also matched a symlink named link.php.
Find by size
find / -xdev -type f -size +100M -exec ls -lh {} +This lists files over 100 MiB on the root filesystem. -xdev keeps find on one filesystem, so it does not descend into /proc or a mounted backup disk.
| Suffix | Unit |
|---|---|
c | Bytes |
k | KiB (1,024 bytes) |
M | MiB (1,048,576 bytes) |
G | GiB |
| none | 512-byte blocks |
+100M means more than 100 MiB, -100M less, and no sign means exactly 100 units. -empty finds empty files and empty directories.
find rounds sizes up to whole units before comparing. -size -1M therefore matches only empty files: in our test it skipped a 500,000-byte file. For under one megabyte, write -size -1048576c, since bytes are never rounded.
Find by age: -mtime, -mmin and -newer
| Test | Matches files modified |
|---|---|
-mtime -1 | Less than 24 hours ago |
-mtime 7 | Between 7 and 8 days ago |
-mtime +7 | 8 or more days ago |
-mmin -60 | In the last 60 minutes |
-daystart -mtime 0 | Since midnight today |
-newer /backups/.last-run | More recently than that file |
-newermt '2026-10-01 12:00' | After that date and time |
find counts age in whole 24-hour periods and drops the fraction, which is why +7 means 8 days or more. In our test, a file 7 days and 12 hours old matched -mtime 7 but not -mtime +7. Dates work too; this finds archives modified from 20 to 25 September:
find /backups -type f -newermt '2026-09-20' ! -newermt '2026-09-26'-mtime uses the content modification time. -ctime is the inode change time (permissions, owner, renames), not the creation time, and -atime is the last access, which most Linux systems record only approximately.
To pick up everything changed since the last run of a job, keep a marker file: find /var/www -type f -newer /backups/.last-run lists the changes, then touch /backups/.last-run resets the marker.
Combine tests: AND, OR, NOT and parentheses
find /var/www -type f \( -name '*.jpg' -o -name '*.mp4' \)Tests side by side are joined by AND. -o is OR, ! (or -not) is NOT, and \( \) group tests; the backslashes stop the shell from reading the parentheses. AND binds tighter than OR, so without the parentheses, find /var/www -type f -name '*.jpg' -o -name '*.mp4' -print printed only the .mp4 file in our test: -print belonged to the second half.
-prune skips a directory entirely. This lists .js files outside node_modules and never reads inside them:
find /var/www -path '*/node_modules' -prune -o -type f -name '*.js' -print! -path '*/node_modules/*' gives the same list but still walks every file under node_modules, which is slow on large trees. -maxdepth 1 stops find going below the starting directory, and -mindepth 1 leaves the starting directory itself out; write them straight after the starting point, since they apply to the whole search.
Find by permissions and owner
| Test | Matches |
|---|---|
-perm 644 | Mode exactly 644. |
-perm -o+w | World-writable. With -, all the listed bits must be set; same as -perm -0002. |
-perm -4000 | Setuid files. |
-perm /6000 | Setuid or setgid. With /, any of the listed bits. |
-user www-data, -group www-data | Owned by that user or group. |
-nouser, -nogroup | Owned by a user or group ID with no name on this system. |
World-writable files under a web root are worth checking for, especially after a restore:
find /var/www -type f -perm -o+w-nouser helps after restoring files from another server: anything it lists has an owner ID that does not exist here. To reset a web root to the usual modes, directories 755 and files 644 (this also clears execute bits on any scripts):
find /var/www/html -type d -exec chmod 755 {} +find /var/www/html -type f -exec chmod 644 {} +Run commands on the results: -exec, -exec + and xargs
-exec runs a command for matches, with {} replaced by the path. Ending it with \; runs the command once per file; ending it with + passes as many paths as fit on one command line, which is much faster for many files. Inside /var/www/html/uploads:
find . -name '*.jpg' -exec echo gzip {} \;gzip ./my photo.jpg
gzip ./medium.jpgfind . -name '*.jpg' -exec echo gzip {} +gzip ./my photo.jpg ./medium.jpgBoth are safe with spaces in names, because find hands each path over as a separate argument. A plain pipe to xargs is not:
find . -name '*.jpg' | xargs ls -lls: cannot access './my': No such file or directory
ls: cannot access 'photo.jpg': No such file or directory
-rw-r--r-- 1 root root 500000 Oct 3 16:26 ./medium.jpgfind . -name '*.jpg' -print0 | xargs -0 -r ls -l-print0ends each path with a null byte, which cannot appear in a file name, andxargs -0splits on it.-rstops xargs running the command at all when there is no input. Without it,xargs ls -ldwith no input listed the current directory in our test; with a destructive command that default is a trap.xargs -P 4runs up to four commands in parallel, which-execcannot do.
Use -exec ... {} + by default, and -print0 | xargs -0 -r when you need parallel runs or a filter in between.
Delete old backups safely
Run the command with -print first and read the list:
find /backups -maxdepth 1 -type f -name 'www-*.tar.gz' -mtime +14 -printWhen the list is right, replace -print with -delete:
find /backups -maxdepth 1 -type f -name 'www-*.tar.gz' -mtime +14 -delete-delete is an action, and find evaluates left to right. Written first, as in find /backups -delete -name '*.tar.gz', it deletes everything under /backups before the name test is ever applied; in our test it removed the starting directory as well. Keep -delete last, with -type f and a specific -name before it.
On directories, -delete works bottom-up, so a directory that becomes empty once its empty subdirectories are gone is removed too. In our test, -type d -empty -print listed one directory and -delete removed two.
Age-based pruning has one dangerous failure mode. If backups stop running, another good archive passes 14 days every day, until -mtime +14 has deleted the last one. Only prune when a fresh backup exists:
#!/bin/sh
if [ -n "$(find /backups -maxdepth 1 -name 'www-*.tar.gz' -mtime -2 -print -quit)" ]; then
find /backups -maxdepth 1 -type f -name 'www-*.tar.gz' -mtime +14 -delete
else
echo "no backup in the last 2 days, not pruning" >&2
exit 1
fi-print -quit stops at the first match, so the check ends early. Alternatively, keep a fixed number of archives rather than a fixed age. This keeps the newest 7, whatever their dates:
find /backups -maxdepth 1 -type f -name 'www-*.tar.gz' -printf '%T@ %p\n' | sort -rn | tail -n +8 | cut -d' ' -f2- | xargs -r -d '\n' rm --%T@ prints the modification time in seconds, sort -rn puts the newest first, and tail -n +8 passes on everything from the 8th line down. Run it without the final | xargs -r -d '\n' rm -- to see what would go. It assumes names without newlines, which holds for archive names you generate yourself.
Print exactly what you need with -printf
-printf formats each match. The 10 largest files under /var, with sizes in bytes:
find /var -xdev -type f -printf '%s\t%p\n' | sort -rn | head -10| Directive | Prints |
|---|---|
%p | The path |
%f | The file name only |
%s | Size in bytes |
%TY-%Tm-%Td | Modification date, such as 2026-10-03 |
%T@ | Modification time in seconds since 1970 |
%u, %g | Owner and group names |
%m | Permissions in octal, such as 644 |
-ls prints an ls -dils style line for each match without starting ls.
Deleting old archives with find is a retention policy in one line. To choose the numbers, see how long to keep backups.
Frequently asked questions
- How do I find files larger than 1 GB?
find / -xdev -type f -size +1G. Add-exec ls -lh {} +to see their sizes.- How do I find files modified in the last 24 hours?
find /path -type f -mtime -1, or-mmin -1440. For files changed since midnight, use-daystart -mtime 0.- What is the difference between -exec {} \; and -exec {} +?
\;runs the command once per file.+passes many files to each run, like xargs, which is much faster on large result sets.- How do I exclude a directory from find?
- Prune it:
find /var/www -path '*/node_modules' -prune -o -type f -print. The-prunebranch skips the directory, and the explicit-printapplies only to the other branch. - Why does find say "paths must precede expression"?
- A pattern was not quoted, so the shell expanded it into several file names. Quote it:
-name '*.php'.
How this was checked
The commands were run on Ubuntu 24.04 LTS, GNU findutils 4.9.0 on October 3, 2026. Any that need something this test server does not have, such as a second server, a cloud account or another database engine, were checked against the official pages below instead.
Sources, on October 3, 2026: