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:
-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/ownership | find | fd doesn't support this |
| 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.
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.