find is the command every admin types daily and almost nobody reads the manual for. The result is a handful of memorised incantations and a lot of surprises: -size -1M that only returns empty files, -mtime +7 that skips files exactly a week old, and a -prune that prints the very directory you meant to exclude.
This is a reference grouped by what you are filtering on. Everything here is GNU findutils, which is what Debian, Ubuntu, RHEL and Fedora ship; BSD and macOS differences are noted at the end. For a wider set of everyday commands see the Linux tools cheat sheet, and for turning plain-English requests into find, awk and sed pipelines see AI-assisted one-liners.
How a find Expression Is Read
The shape is find [starting-points] [expression]. For every file under each starting point, find evaluates the expression left to right. Adjacent tests are joined by an implicit -a (AND) and evaluation short-circuits, so a failed test stops the rest. If the expression contains no action, -print is added for you.
$ find /etc -name '*.conf' # one start point, one test
$ find /var/log /tmp -type f -name '*.gz' # two start points, implicit AND
$ find . -name '*.log' -o -name '*.txt' # OR
$ find . \( -name '*.log' -o -name '*.txt' \) -mtime -1 # group, then AND
$ find . ! -name '*.bak' # NOT (same as -not)
$ find / -name passwd 2>/dev/null # hide "Permission denied" noise
Quote every pattern β an unquoted *.conf is expanded by the shell before find sees it whenever a matching file exists in the current directory. The result is a confusing "paths must precede expression" error or a silently wrong search.
AND binds tighter than OR β find . -name a -o -name b -print prints only the b matches, because it parses as -name a -o ( -name b -a -print ). Group with parentheses and put the action outside the group.
Escape the parentheses β the shell treats bare parentheses as a subshell. Write \( and \) or quote them as '(' and ')', with spaces on both sides.
Matching by Name and Path
| Test | Matches against | Example |
|---|---|---|
-name pat | Basename only, shell glob, case-sensitive | find /etc -name '*.conf' |
-iname pat | Basename, case-insensitive | find ~ -iname '*.jpg' |
-path pat | Whole path; * also matches / | find . -path '*/cache/*' |
-ipath pat | Whole path, case-insensitive | find / -ipath '*/readme*' |
-regex pat | Whole path, Emacs regex by default | find . -regex '.*\.\(jpg\|png\)' |
-regextype posix-extended | Switches later -regex tests to ERE | find . -regextype posix-extended -regex '.*\.(jpg|png)' |
-lname pat | Target of a symlink | find /usr/bin -lname '*python3*' |
-samefile f | Every hard link to the same inode as f | find / -xdev -samefile /usr/bin/perl |
-inum n | Inode number n | find . -inum 1835023 |
-name never sees slashes β it matches the last path component only, so -name 'conf.d/*.conf' can never match; GNU find warns about it. Use -path '*/conf.d/*.conf'.
-regex is anchored to the full path β the pattern must match all of ./src/app/logo.png, not just part of it, which is why regex patterns almost always start with .*.
Filtering by Type and Size
| Test | Meaning |
|---|---|
-type f | Regular file |
-type d | Directory |
-type l | Symbolic link (the link itself, not its target) |
-type s / p | Socket / named pipe (FIFO) |
-type b / c | Block device / character device |
-xtype l | Broken symlinks (the target type is checked, and a dangling link stays l) |
-empty | Zero-byte regular files and directories with no entries |
-size +100M | Larger than 100 MiB |
-size -10k | Smaller than 10 KiB, after rounding up (see below) |
-size 0 | Exactly zero 512-byte blocks, i.e. empty |
The size suffixes are c bytes, k KiB, M MiB, G GiB, w two-byte words and b 512-byte blocks, which is also the default when there is no suffix. The trap is rounding: find rounds each file size up to whole units before comparing. A 300 KB file counts as 1M, so -size -1M ("less than one") only matches files of size zero. For anything below one unit, use a smaller unit or bytes.
$ find / -xdev -type f -size +500M -exec ls -lh {} + 2>/dev/null
$ find /var/log -type f -size +100M -size -1G # between 100 MiB and 1 GiB
$ find . -type f -size -1048576c # really under 1 MiB
$ find . -type f -empty # zero-byte files
$ find . -type d -empty -delete # prune empty directories
$ find /etc /usr/local -xtype l # dangling symlinks
$ find /srv -type f -size +1G -printf '%s\t%p\n' | sort -rn | head
5368709120 /srv/backups/db-2026-09-28.dump
2147483648 /srv/vm/disk1.img
Filtering by Time
| Test | Unit | Timestamp |
|---|---|---|
-mtime n | 24-hour periods | Content last modified |
-mmin n | Minutes | Content last modified |
-atime n / -amin n | Days / minutes | Last read (unreliable on relatime or noatime mounts) |
-ctime n / -cmin n | Days / minutes | Inode change: content, owner, mode, link count. Not creation time |
-newer f | Reference file | Modified more recently than f |
-newermt date | Date string | Modified after the given date or time |
-daystart | Option | Measure later time tests from today's midnight, not from now |
For the day-based tests, find divides the file's age by 24 hours and throws away the fraction before comparing. That makes the three forms behave like this:
| Expression | Matches files whose age is | Common reading |
|---|---|---|
-mtime 0 | Under 24 hours | Modified in the last day |
-mtime -7 | Under 7 days (truncated age 0 to 6) | Modified this week |
-mtime 7 | At least 7 but under 8 days | Exactly a week old |
-mtime +7 | At least 8 days (truncated age above 7) | Older than a week, and one day later than most people expect |
$ find /var/log -type f -name '*.gz' -mtime +30 # rotated logs older than ~a month
$ find /etc -type f -mmin -60 # config changed in the last hour
$ find . -daystart -mtime 0 # modified since midnight
$ find /data -newermt '2026-09-01' ! -newermt '2026-09-08' # a date range
$ touch -d '2 hours ago' /tmp/ref && find /srv -newer /tmp/ref
$ find /home -type f -mmin -30 -printf '%TY-%Tm-%Td %TH:%TM %p\n'
-daystart is positional β it only affects tests that come after it on the command line, so find . -mtime 0 -daystart does not do what you want.
There is no creation time test in classic find β ext4 and XFS record a birth time, and recent coreutils shows it as Birth: in stat. For finding things, -newermt on mtime is what works everywhere.
Owners, Permissions and Links
| Test | Matches | Example |
|---|---|---|
-user u / -uid n | Owned by user name or UID | find /home -user alice |
-group g / -gid n | Owned by group | find /srv -group www-data |
-nouser / -nogroup | UID or GID with no passwd/group entry | find / -xdev -nouser |
-perm 644 | Mode is exactly 644 | find . -type f -perm 644 |
-perm -644 | All of these bits set (others may be too) | find . -perm -u+x |
-perm /022 | Any of these bits set | find . -perm /go+w |
-readable / -writable / -executable | Accessible to the user running find (uses access(2)) | find /etc -writable |
-links n | Hard link count | find . -type f -links +1 |
$ find / -xdev -type f -perm -4000 # SUID binaries
$ find / -xdev -type f -perm /6000 # SUID or SGID
$ find /var/www -type f -perm /o+w # world-writable files
$ find / -xdev \( -nouser -o -nogroup \) -ls # left behind after userdel
$ find /srv/share -type d ! -perm -g+x # dirs the group cannot enter
$ find /home -user alice -type f -size +1G # one user's big files
Minus means all, slash means any β -perm -6000 needs both SUID and SGID; -perm /6000 needs either. The old -perm +6000 form for "any" is no longer accepted by GNU find.
Symbolic modes work β -perm -g+w and -perm /u=x,g=x are valid and often clearer than octal. The chmod calculator converts between the two, and our permissions guide explains what the special bits do.
Controlling the Walk
$ find /etc -maxdepth 1 -type f -name '*.conf' # top level only
$ find . -mindepth 2 -maxdepth 3 -type d # skip the first level
$ find / -xdev -type f -size +1G # stay on this filesystem
$ find . -path ./node_modules -prune -o -type f -name '*.js' -print
$ find . \( -name .git -o -name node_modules \) -prune -o -type f -print
$ find -L /opt/app -type f -name '*.jar' # follow symlinks
$ find / -name sshd_config -print -quit 2>/dev/null # stop at the first match
-prune needs an explicit -print β without it, the implicit -print wraps the whole expression and the pruned directory names are printed too. The idiom is always ... -prune -o ... -print.
! -path is not pruning β find . ! -path './node_modules/*' hides results but still walks every file below node_modules. On a large tree -prune is dramatically faster.
-maxdepth goes first β it is a global option. GNU find warns if you put it after a test, because it applies to the whole search regardless of position.
-xdev on whole-system searches β it keeps find out of /proc, /sys, network mounts and other filesystems. -mount is an older synonym.
Acting on Results: -exec, xargs, -delete, -printf
| Action | What it does |
|---|---|
-print / -print0 | Print the path, newline- or NUL-terminated |
-ls | ls -dils style line for each match |
-exec cmd {} \; | Run cmd once per file |
-exec cmd {} + | Run cmd with as many files per call as fit (like xargs) |
-execdir cmd {} + | Same, but run from the file's own directory |
-ok cmd {} \; | Ask for confirmation before each run |
-delete | Remove the file or empty directory; implies -depth |
-printf fmt | Custom output with format directives |
-quit | Exit immediately, usually after the first match |
$ find . -name '*.tmp' -exec rm -f {} + # one rm per batch of files
$ find . -name '*.sh' -exec chmod 755 {} \; # one chmod per file
$ find . -type f -name '*.log' -print0 | xargs -0 -P4 gzip # parallel, safe for any name
$ find /var/log/app -name '*.log' -mtime +14 -print # dry run first...
$ find /var/log/app -name '*.log' -mtime +14 -delete # ...then swap -print for -delete
$ find /etc -type f -printf '%TY-%Tm-%Td %u %m %s %p\n' | sort | tail -3
2026-09-28 root 644 1203 /etc/ssh/sshd_config
2026-09-30 root 640 4512 /etc/sudoers.d/ops
2026-10-01 root 644 287 /etc/hosts
| -printf directive | Output |
|---|---|
%p / %f / %h | Full path / basename / parent directory |
%s / %k | Size in bytes / disk usage in 1 KiB blocks |
%m / %M | Octal mode / ls-style mode (-rw-r--r--) |
%u / %g | Owner / group name |
%i / %n | Inode number / hard link count |
%l | Symlink target (empty for other types) |
%T@ / %TY-%Tm-%Td | mtime as epoch seconds / as a date (A and C prefixes give atime and ctime) |
Never put -delete first β the expression is evaluated in order, so find . -delete -name '*.tmp' deletes everything and then tests the name. Write the search, run it with -print, then append -delete at the end.
-delete and -prune do not mix β -delete turns on -depth, and -prune has no effect under -depth. Use -exec rm if you also need to skip directories.
Prefer -print0 with xargs β plain find | xargs breaks on spaces and newlines in filenames. -print0 | xargs -0, or -exec ... +, handles every legal name.
BSD and macOS find β there is no -printf or -regextype; use -E for extended regex and pipe to stat -f for custom output. Installing GNU findutils through a package manager provides gfind.
Name-only searches across the disk β locate (plocate or mlocate depending on distro) queries an index rebuilt by updatedb, usually daily. It is near-instant but misses files created since the last run.
Frequently Asked Questions
What is the difference between -exec {} \; and -exec {} + in find?
With \; find runs the command once for every matching file, which is slow for thousands of files. With + it appends as many paths as fit on one command line and runs the command a few times, the same way xargs does. Use + whenever the command accepts multiple file arguments.
Why does find -size -1M only return empty files?
find rounds each file size up to whole units before comparing. Any non-empty file smaller than a megabyte rounds up to 1M, which is not less than 1, so only zero-byte files match. Use a smaller unit such as -size -1024k or exact bytes such as -size -1048576c.
How do I exclude a directory from a find search?
Use -prune: find . -path ./node_modules -prune -o -type f -print. The explicit -print at the end matters, otherwise the pruned directory itself is printed. Filtering with ! -path hides the results but still walks the whole directory, which is much slower on large trees.
Does find -mtime +7 mean older than 7 days?
Not exactly. find truncates each file's age to whole days, and +7 means the truncated value is greater than 7, so a file must be at least 8 days old to match. If you need precise hours, use -mmin +10080 for 7 days in minutes.
How do I safely delete files with find?
Build the expression with -print first and check the list. When it is right, replace -print with -delete at the end of the same command. Never place -delete before the tests, because find evaluates left to right and would delete every file it visits. Add -xdev when starting from a high-level directory.