Of every command in this series, find is the one I’ve spent the most time actually mastering, because it rewards it more than most — it’s less a single-purpose search tool and more a small filtering and action-execution language built around the filesystem. Once the expression syntax clicks, find stops being “the thing I use to search for files by name” and becomes a genuinely powerful automation tool for cleanup, auditing, and bulk operations. This guide walks through the whole thing.
What Is the find Command?
find searches a directory tree, in real time, evaluating every file and directory it encounters against a set of tests you specify — name, type, size, modification time, permissions, ownership, and more — and either prints matches or performs an action on them. Unlike locate, which searches a prebuilt database, find always reflects the current, live state of the filesystem, at the cost of being slower for broad searches since it has to traverse directories in real time.
Basic Syntax
find [PATH...] [EXPRESSION]
The expression is a combination of tests, operators, and actions, evaluated left to right for every file find encounters during its traversal.
A Basic Example
$ mkdir -p findtest/sub1 findtest/sub2
$ touch findtest/sub1/a.txt findtest/sub1/b.log findtest/sub2/c.txt
$ find findtest -type f
findtest/sub2/c.txt
findtest/sub1/b.log
findtest/sub1/a.txt
-type f restricts results to regular files (as opposed to directories, symlinks, etc.).
Core Test Options
| Test | Description |
|---|---|
-name PATTERN | Match filename against a glob pattern, case-sensitive |
-iname PATTERN | Case-insensitive version of -name |
-type TYPE | Filter by type: f (file), d (directory), l (symlink), b/c (device), p (pipe), s (socket) |
-size N[cwbkMG] | Filter by size (+N larger than, -N smaller than, N exactly) |
-mtime N | Modified N*24-hour periods ago (+N older than, -N newer than) |
-mmin N | Modified N minutes ago, finer-grained than -mtime |
-atime N / -amin N | Same, but for last access time |
-ctime N / -cmin N | Same, but for metadata change time |
-newer FILE | Modified more recently than FILE |
-user NAME | Owned by user NAME |
-group NAME | Owned by group NAME |
-perm MODE | Match specific permission bits |
-empty | Empty files or directories |
-maxdepth N | Limit recursion depth |
-mindepth N | Skip results above a certain depth |
-inum N | Match a specific inode number |
-links N | Match files with N hard links |
Filtering by Name
$ find findtest -name "*.txt"
findtest/sub2/c.txt
findtest/sub1/a.txt
Case-insensitive matching:
$ find findtest -iname "*.TXT"
findtest/sub2/c.txt
findtest/sub1/a.txt
Filtering by Modification Time
$ find findtest -mtime -1
findtest
findtest/sub2
findtest/sub2/c.txt
findtest/sub1
findtest/sub1/b.log
findtest/sub1/a.txt
-mtime -1 means “modified less than 1 day ago” — every file and directory created moments ago in the example matches, including the top-level directory itself, because find by default includes the starting path as a candidate too.
For finer control than whole days, -mmin works in minutes — genuinely useful when hunting for files changed in the last few minutes during an active investigation:
$ find /var/log -mmin -10
Filtering by Size
$ find findtest -size +0c
findtest
findtest/sub2
findtest/sub1
Interesting result here: in this environment the touched files are zero bytes, so only the directories (which have nonzero size themselves as directory entries) match +0c (larger than 0 bytes). For finding large files on a server, a much more typical real-world invocation looks like:
$ find /var -size +100M
Size suffixes: c for bytes, k for kilobytes, M for megabytes, G for gigabytes, and no suffix defaults to 512-byte blocks (a historical quirk worth remembering, since it surprises people who assume bytes by default).
Taking Action: -exec
This is where find goes from a search tool to an automation tool — running an arbitrary command against every match:
$ find findtest -name "*.log" -exec ls -l {} \;
-rw-r--r-- 1 root root 0 Jul 31 01:36 findtest/sub1/b.log
{} is replaced with the current match’s path, and \; terminates the command (escaped so the shell doesn’t interpret the semicolon itself). This runs the command once per match, which is correct but can be slow for a huge result set since it spawns a new process every time.
A faster variant, -exec ... +, batches as many matches as possible into a single command invocation (much like xargs does), significantly reducing process-spawn overhead:
$ find findtest -name "*.log" -exec ls -l {} +
Combining Tests with Logical Operators
find expressions support boolean logic explicitly:
| Operator | Meaning |
|---|---|
-a (or implicit) | AND — both tests must match |
-o | OR — either test matches |
! or -not | NOT — negate a test |
( ... ) | Group tests to control precedence (parentheses must be escaped or quoted in the shell) |
$ find /var/log -name "*.log" -a -mtime -7
$ find . -name "*.tmp" -o -name "*.bak"
$ find . ! -name "*.conf"
$ find . \( -name "*.log" -o -name "*.tmp" \) -mtime +30
That last example finds files that are either .log or .tmp, AND older than 30 days — a classic log-cleanup pattern, where the parentheses are essential to get the operator precedence right (without them, -mtime +30 would only apply to the .tmp branch, not both).
How find Works Internally
find performs a recursive directory tree walk starting from each given path, using the readdir()/opendir() family of syscalls to enumerate directory entries and stat() (or lstat() when symlinks shouldn’t be followed) to retrieve metadata for each entry — size, modification time, ownership, permission bits, inode number, and file type. Every test in your expression is evaluated against this metadata, in the order given, short-circuiting where possible (an early failing -name test skips evaluating a later, more expensive -exec for that file).
This live-metadata-per-file approach is exactly why find is always accurate (there’s no staleness window like locate‘s database) but slower for broad searches — every single file and directory in the traversed tree incurs at least one syscall, and directories with many entries or deeply nested trees on slow storage (network filesystems especially) can make a broad find / search take a genuinely long time.
find‘s traversal order is generally depth-first, and by default it doesn’t follow symbolic links when descending into directories (avoiding potential infinite loops from circular symlinks), though -L can be used to explicitly follow them if that’s what you want.
Real-World Use Cases
1. Cleaning Up Old Log Files
$ find /var/log -name "*.log" -mtime +30 -delete
-delete is a built-in action — faster and safer than piping to xargs rm for straightforward deletions, though it requires -depth-like ordering internally (which find handles automatically) to correctly delete directories before… actually after their contents.
2. Finding and Fixing Insecure Permissions
$ find /var/www -type f -perm /o+w -exec chmod o-w {} +
This finds world-writable files and strips the world-write bit — a common security hardening task.
3. Locating Large Files Eating Disk Space
$ find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null
-xdev prevents find from crossing into other mounted filesystems (like network mounts or other partitions), keeping the search scoped to the current filesystem — important for accurate disk-usage investigation.
4. Finding Files Owned by a Deleted User
$ find / -xdev -nouser
-nouser matches files whose owner UID doesn’t correspond to any entry in /etc/passwd — a classic sign of leftover files from a removed account.
5. Building a Backup File List
$ find /data -type f -newer /var/backups/last_backup_marker > files_to_backup.txt
6. Finding and Removing Empty Directories
$ find /tmp/workdir -type d -empty -delete
Shell Scripting and Automation
A defensive cleanup script that’s careful about what it deletes, logging before acting:
#!/bin/bash
# cleanup_old_temp.sh - remove temp files older than 7 days, with a dry-run mode
set -euo pipefail
TARGET_DIR="/var/tmp/myapp"
DRY_RUN="${1:-}"
if [[ "$DRY_RUN" == "--dry-run" ]]; then
echo "Dry run - files that WOULD be deleted:"
find "$TARGET_DIR" -type f -mtime +7 -print
else
find "$TARGET_DIR" -type f -mtime +7 -print -delete
fi
And a variant using -print0/xargs -0 for safely handling filenames with spaces or special characters when the action is more complex than a simple -exec can express cleanly:
find /data -type f -name "*.csv" -print0 | xargs -0 -I{} md5sum {} > checksums.txt
find vs Related Commands
| Command | Purpose |
|---|---|
find | Real-time, richly filtered filesystem traversal and action execution |
locate | Fast filename-only search against a prebuilt, periodically-refreshed database |
fd | Modern, ergonomic find alternative with saner defaults and better performance for typical interactive use, though less universally preinstalled |
grep -r | Searches file content, not metadata; often combined with find for content-aware pipelines |
du | Reports disk usage by directory, complementary to find -size for space investigations |
xargs | Frequently paired with find -print0 for batch operations beyond what -exec conveniently expresses |
Troubleshooting Common Issues
Problem: find is extremely slow on a large or networked filesystem. Use -maxdepth to limit recursion where you know the target is shallow, -xdev to avoid crossing into other mounted filesystems, and consider locate instead if real-time accuracy isn’t required for the specific query.
Problem: “Permission denied” errors clutter the output. find reports errors for directories it can’t read (common when searching from / as a non-root user). Redirect stderr to suppress the noise: find / -name "*.conf" 2>/dev/null.
Problem: -exec runs once per file and is unbearably slow for thousands of matches. Switch from -exec cmd {} \; (one process per match) to -exec cmd {} + (batched, far fewer processes) wherever the target command supports multiple arguments.
Problem: Filenames with spaces or newlines break a pipeline. Use -print0 paired with xargs -0 (or a while IFS= read -r -d '' loop) instead of plain newline-separated output whenever filenames might contain unusual characters.
Problem: find ... -delete didn’t delete a non-empty directory. -delete requires directories to be empty before removal, and find‘s traversal must process children before the parent — this happens automatically with default depth-first traversal, but if you’ve added -prune or reordered tests in a way that skips descending into a directory, deletion of that directory can fail.
Performance Optimization
- Prefer
-maxdepth/-mindepthto bound the search space whenever you know roughly where results will be. - Put cheaper, more selective tests earlier in the expression —
findshort-circuits, so an early-namefilter that eliminates most candidates before a later, more expensive-execruns saves real time. - Use
-exec ... +(batched) instead of-exec ... \;(per-match) whenever the invoked command supports multiple file arguments. - Use
-xdevto avoid unnecessarily traversing into other mounted filesystems, network shares, or bind mounts. - For very frequent, broad searches where slight staleness is acceptable,
locatewill consistently outperformfindby a wide margin — reservefindfor cases where filtering richness or real-time accuracy actually matters.
Security Implications
find is frequently used in security auditing (finding world-writable files, SUID/SGID binaries, files owned by no valid user) but is equally capable of being destructive if misused — -delete and -exec rm {} + are permanent, unconfirmed operations. Before running any find command with a destructive action against a broad path, run the equivalent -print (or the dry-run pattern shown above) first to review exactly what would be affected.
-perm searches are a standard part of hardening audits:
$ find / -xdev -perm -4000 -type f # find SUID binaries
$ find / -xdev -perm -2000 -type f # find SGID binaries
$ find / -xdev -perm -0002 -type f # find world-writable files
Reviewing SUID/SGID binaries periodically is a standard security practice, since an unexpected SUID binary can be a sign of a compromised system or a misconfigured install.
Compatibility Across Distributions
find is part of GNU findutils and ships by default on every major Linux distribution — Ubuntu, Debian, Fedora, RHEL, Arch, openSUSE — with consistent behavior since they share the same GNU implementation. BSD and macOS ship a different, POSIX-based find with a smaller and somewhat differently-behaved flag set (notably, BSD find‘s -mtime and certain other time-based tests can have subtly different rounding/interpretation, and long-form GNU-only flags like -printf aren’t available). Scripts intended to run identically on both should stick to POSIX-standard tests (-name, -type, -exec, -size) and test explicitly on the target platform before relying on GNU-specific extensions.
Best Practices
- Always dry-run (
-printinstead of-delete/-exec rm) before any destructivefindoperation on a broad path. - Use
-print0/xargs -0for filenames that might contain spaces or special characters. - Bound searches with
-maxdepthand-xdevwhere appropriate, both for performance and to avoid unintended scope creep into other filesystems. - Prefer batched
-exec ... +over per-match-exec ... \;for performance when working with large result sets. - Use
2>/dev/nulldeliberately and knowingly, not reflexively — suppressing permission-denied errors can also hide genuinely useful diagnostic information.
Summary
find is the most expressive filesystem-search tool in the standard Linux toolkit, combining real-time accuracy with a rich set of composable tests — name, type, size, time, ownership, permissions — and the ability to act directly on matches via -exec or -delete. Its trade-off against locate is speed for accuracy: find always reflects the current filesystem state, at the cost of traversal overhead on broad searches. Mastering its expression syntax, especially logical grouping and the -print0/batched--exec safety patterns, turns it into one of the most powerful single commands available for system administration and automation.
References
- GNU Findutils Manual — https://www.gnu.org/software/findutils/manual/html_mono/find.html
- Linux man-pages project:
man 1 find— https://man7.org/linux/man-pages/man1/find.1.html - Ubuntu Manpage Repository — https://manpages.ubuntu.com/manpages/noble/en/man1/find.1.html
- GNU Findutils Manual:
xargs— https://www.gnu.org/software/findutils/manual/html_mono/find.html#Invoking-xargs
