The find command recursively walks a directory tree and returns every file or directory matching your criteria. It handles name patterns, timestamps, sizes, permissions, and can act on results directly — no piping required.
In short: find /path -name "*.log" -type f locates all .log files under /path. Add -exec to act on matches, -mtime to filter by age, -size to filter by size, and -prune to skip directories. Combine conditions with -and, -or, and -not for precise queries.
Why find Is Still Essential
You might wonder: why learn find when fd exists and is 20x faster for simple searches? Because find does things fd doesn't:
- It's everywhere. Every Linux box, every Docker container, every CI runner has
find. No installation needed. -execis built in. You can act on results without piping to xargs.- Complex conditions. AND, OR, NOT, grouping with parentheses —
findis a query language for your filesystem. - Timestamp precision. Filter by modification, access, or status-change time down to the minute.
- Permission-based searches. Find SUID binaries, world-writable files, files owned by a specific user.
I still use fd for quick fuzzy searches, but when I need to clean up a build server or audit file permissions on a production box, find is the tool I reach for. On a 32blog deploy, I used find to track down stale .next cache files that were eating 4GB of disk — fd couldn't filter by size and modification time in one pass.
The Basics: Name, Type, and Path
Search by name
# Find all TypeScript files
find . -name "*.ts"
# Case-insensitive search
find . -iname "*.readme"
-name uses shell glob patterns (not regex). The pattern must match the entire filename — "*.ts" matches app.ts but not app.tsx.
Filter by type
# Only files (not directories)
find . -name "*.log" -type f
# Only directories
find /etc -type d -name "nginx"
# Only symbolic links
find /usr/local/bin -type l
Common -type values: f (file), d (directory), l (symlink), b (block device), c (character device).
Start from a specific path
# Search in multiple directories
find /var/log /tmp -name "*.log" -type f
# Search from root (can be slow — consider -maxdepth)
find / -name "nginx.conf" 2>/dev/null
Filtering by Time: -mtime, -newer, and Friends
Time-based filtering is where find really shines. Three timestamps exist for every file:
| Option | Timestamp | Meaning |
|---|---|---|
-mtime | mtime | Content modification time |
-atime | atime | Last access (read) time |
-ctime | ctime | Metadata change time (permissions, owner) |
The +/- notation
The number after -mtime means "24-hour periods ago":
# Modified MORE than 30 days ago
find /var/log -name "*.log" -mtime +30
# Modified LESS than 1 day ago (within last 24 hours)
find . -name "*.mdx" -mtime -1
# Modified EXACTLY 7 days ago (between 168 and 192 hours ago)
find . -mtime 7
Minute-level precision
Replace -mtime with -mmin for minutes instead of days:
# Files modified in the last 15 minutes
find . -mmin -15
# Files NOT accessed in the last 60 minutes
find . -amin +60
Newer than a reference file
# Files modified after deploy.log was last modified
find . -newer deploy.log
# Create a timestamp marker, then find files newer than it
touch -t 202603200000 /tmp/marker
find /var/log -newer /tmp/marker
Filtering by Size and Permissions
By size
# Files larger than 100MB
find / -type f -size +100M
# Files smaller than 1KB
find . -type f -size -1k
# Empty files
find . -type f -empty
# Empty directories
find . -type d -empty
Size suffixes: c (bytes), k (kilobytes), M (megabytes), G (gigabytes).
By permissions
# World-writable files (security audit)
find / -type f -perm -o=w 2>/dev/null
# SUID binaries (security audit)
find / -type f -perm -4000 2>/dev/null
# Files with exact permissions 644
find . -type f -perm 644
# Files where owner can execute
find . -type f -perm -u=x
The -perm notation (see the chmod/chown guide for permission basics):
-perm 644— exact match (must be exactly 644)-perm -644— all these bits must be set (may have more)-perm /644— any of these bits set
By ownership
# Files owned by user "deploy"
find /var/www -user deploy
# Files owned by group "www-data"
find /var/www -group www-data
# Files with no owner (orphaned — user was deleted)
find / -nouser 2>/dev/null
Taking Action: -exec, -delete, and -print0
Finding files is only half the job. find can act on results directly.
-exec with ;
Runs the command once per file:
# Change permissions on all shell scripts
find . -name "*.sh" -exec chmod +x {} \;
# Show detailed info for each .conf file
find /etc -name "*.conf" -exec ls -la {} \;
The {} is replaced by the current filename. The \; terminates the command (the backslash escapes the semicolon from the shell).
-exec with + (batch mode)
Passes as many filenames as possible in one command invocation — much faster:
# Count lines across all Python files (one wc call)
find . -name "*.py" -exec wc -l {} +
# Change owner on all files at once
find /var/www -type f -exec chown deploy:www-data {} +
The + terminator groups arguments like xargs does. Use this whenever the target command accepts multiple arguments.
-delete
A built-in shorthand for removing matched files:
# Delete all .tmp files
find /tmp -name "*.tmp" -type f -delete
# Delete empty directories
find ./build -type d -empty -delete
-print0 for safe piping
When piping to xargs, always use -print0 to handle filenames with spaces or special characters:
# Safe pipeline: null-byte separated
find . -name "*.log" -print0 | xargs -0 gzip
# Without -print0, "my log.txt" breaks into "my" and "log.txt"
Controlling Depth and Pruning Directories
Limit search depth
# Only current directory (no recursion)
find . -maxdepth 1 -name "*.txt"
# Current directory + one level down
find . -maxdepth 2 -type f -name "*.json"
# Skip the first level (only search subdirectories)
find . -mindepth 2 -name "*.ts"
Prune directories
-prune tells find to skip entire directory trees — essential for performance:
# Search for .ts files but skip node_modules
find . -path "*/node_modules" -prune -o -name "*.ts" -print
# Skip multiple directories
find . \( -path "./.git" -o -path "./node_modules" -o -path "./.next" \) -prune -o -name "*.ts" -print
The pattern is: -path "dir_to_skip" -prune -o <your_conditions> -print. The -o means "or" — find either prunes the directory or continues matching.
For a Next.js project like 32blog, skipping .next, node_modules, and .git cuts search time from 12 seconds to under 1 second on a typical project.
Combining Conditions: AND, OR, NOT
By default, multiple conditions are AND-ed together:
# Files that are BOTH .log AND larger than 10MB (implicit AND)
find /var/log -name "*.log" -size +10M
# Explicit AND (same result)
find /var/log -name "*.log" -and -size +10M
OR conditions
# .jpg OR .png files
find . -name "*.jpg" -o -name "*.png"
# With action — must use grouping (parentheses)
find . \( -name "*.jpg" -o -name "*.png" \) -exec ls -la {} +
NOT conditions
# All files that are NOT .git related
find . -not -path "*/.git/*"
# Shorthand: ! instead of -not
find . ! -name "*.tmp"
Complex grouping
# TypeScript files larger than 50KB that were modified today, excluding tests
find src/ \( -name "*.ts" -o -name "*.tsx" \) \
-size +50k -mtime 0 \
-not -path "*/__tests__/*" \
-not -name "*.test.*"
Real-World Patterns
Pattern 1: Clean up old build artifacts
# Delete .next cache files older than 7 days
find .next/cache -type f -mtime +7 -delete
# Remove node_modules from all projects in ~/projects
find ~/projects -maxdepth 3 -name "node_modules" -type d -prune -exec rm -rf {} +
Pattern 2: Find large files eating disk space
# Top 20 largest files on the system
find / -type f -size +50M -exec ls -lhS {} + 2>/dev/null | head -20
# Large files in /var that haven't been accessed in 90 days
find /var -type f -size +100M -atime +90 2>/dev/null
Pattern 3: Security audit
# Find SUID/SGID binaries
find / -type f \( -perm -4000 -o -perm -2000 \) -exec ls -la {} + 2>/dev/null
# World-writable directories (potential security risk)
find / -type d -perm -o=w -not -path "/tmp/*" -not -path "/var/tmp/*" 2>/dev/null
# Files modified in the last 24 hours (incident response)
find / -type f -mtime 0 -not -path "/proc/*" -not -path "/sys/*" 2>/dev/null
Pattern 4: Batch file operations for web projects
# Convert all PNGs to WebP (using -exec with a shell)
find public/images -name "*.png" -exec sh -c 'cwebp -q 80 "$1" -o "${1%.png}.webp"' _ {} \;
# Fix line endings in all source files
find src/ -name "*.ts" -exec dos2unix {} +
# Find duplicate filenames across locales
find content/ -name "*.mdx" -printf "%f\n" | sort | uniq -d
Pattern 5: Git cleanup
# Find untracked large files before committing
find . -maxdepth 3 -size +5M -not -path "./.git/*" -not -path "*/node_modules/*" -type f
# Find files that should be in .gitignore
find . -name ".env*" -o -name "*.pem" -o -name "*.key" | grep -v ".git/"
Pattern 6: Log management
# Compress logs older than 7 days
find /var/log -name "*.log" -mtime +7 -exec gzip {} \;
# Delete compressed logs older than 90 days
find /var/log -name "*.log.gz" -mtime +90 -delete
# Find logs modified in the last hour (troubleshooting)
find /var/log -name "*.log" -mmin -60
find vs fd: When to Use Which
fd is a modern alternative written in Rust that's significantly faster for pattern-based searches. Here's when I use each:
| Scenario | Use | Why |
|---|---|---|
| Quick file search by name | fd | ~20x faster, simpler syntax |
| Filter by permissions (-perm) | find | fd doesn't support this (it has --owner for ownership) |
| Complex time-based queries | find | -mmin, -newer, etc. |
Act on results with -exec | find | Built-in, no pipe needed |
Respect .gitignore | fd | Automatic by default |
| Script portability | find | Available on every system |
| CI/CD pipelines | find | No extra installation |
# fd equivalent of a simple find
fd "\.ts$" src/ # fd: fast, clean
find src/ -name "*.ts" # find: compatible everywhere
# Only find can do this
find / -type f -perm -4000 -mtime -1 -exec ls -la {} +
For more on fd and other modern CLI tools, check Modern Rust CLI Tools. For the complete reference on every find option, the GNU findutils manual is the most thorough resource.
FAQ
What's the difference between -name and -path?
-name matches only the filename (last component). -path matches the entire path from the starting point. Use -path when you need to match directory structure: find . -path "*/config/*.json".
How do I use regex with find instead of globs?
Use -regex instead of -name. By default, find uses Emacs-style regex and matches against the full path:
# Find .ts or .tsx files using regex
find . -regex ".*\.\(ts\|tsx\)$"
# Use extended regex (POSIX ERE) for cleaner syntax
find . -regextype posix-extended -regex ".*\.(ts|tsx)$"
Why does find show "Permission denied" errors?
You're searching directories you don't have read access to. Redirect stderr: find / -name "file" 2>/dev/null. Or use -readable (GNU extension) to skip unreadable entries: find / -readable -name "file".
What's the difference between -exec ; and -exec +?
\; runs the command once per file. + batches files into fewer invocations (like xargs). The + variant is almost always faster. Use \; only when you need per-file behavior (like renaming with a shell command).
How do I make find follow symbolic links?
Use -L flag: find -L /path -name "*.conf". By default, find does not follow symlinks. Be careful — following symlinks can cause infinite loops if there are circular links.
Is find -delete safe to use?
It's safe if you test your query with -print first. find -delete processes files depth-first and removes them permanently — there's no trash/recycle bin. Always run find <conditions> -print before replacing -print with -delete.
How do I search for files by content (not name)?
find itself doesn't search file contents. Combine it with grep:
find . -name "*.ts" -exec grep -l "useState" {} +
Or use grep -r / rg directly for content searches.
Can I limit how many results find returns?
find doesn't have a built-in limit, but you can pipe to head:
find . -name "*.log" | head -5
Note that find continues running after head gets enough results (it receives a SIGPIPE). For large searches, -quit (GNU extension) stops immediately after the first match: find . -name "target" -print -quit.
Wrapping Up
find is one of those commands that rewards memorization. The patterns you'll use most:
find . -name "*.ext" -type f— the bread and butter, locate files by namefind . -mtime +30 -delete— time-based cleanupfind . -path "*/node_modules" -prune -o -name "*.ts" -print— skip directories for speedfind . -name "*.log" -exec gzip {} +— batch actions without pipingfind . -name "*.txt" -print0 | xargs -0 command— safe pipeline to xargs
If you already know xargs and grep, adding find completes the file-processing trifecta. Combine them with sed/awk for transformations and fzf for interactive selection, and you've got a workflow that handles most filesystem tasks without leaving the terminal.