32blogby StudioMitsu

find Practical Guide: Search Your Filesystem Like a Pro

Master the find command to locate files by name, size, date, and permissions. Covers -exec for actions, -prune for speed, condition logic, and real-world patterns for cleanup and automation.

13 min read
On this page

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.
  • -exec is built in. You can act on results without piping to xargs.
  • Complex conditions. AND, OR, NOT, grouping with parentheses — find is 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

bash
# 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

bash
# 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

bash
# 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:

OptionTimestampMeaning
-mtimemtimeContent modification time
-atimeatimeLast access (read) time
-ctimectimeMetadata change time (permissions, owner)

The +/- notation

The number after -mtime means "24-hour periods ago":

bash
# 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:

bash
# 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

bash
# 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

bash
# 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

bash
# 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

bash
# 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:

bash
# 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:

bash
# 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:

bash
# 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:

bash
# 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

bash
# 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:

bash
# 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:

bash
# 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

bash
# .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

bash
# All files that are NOT .git related
find . -not -path "*/.git/*"

# Shorthand: ! instead of -not
find . ! -name "*.tmp"

Complex grouping

bash
# 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

bash
# 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

bash
# 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

bash
# 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

bash
# 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

bash
# 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

bash
# 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:

ScenarioUseWhy
Quick file search by namefd~20x faster, simpler syntax
Filter by permissions/ownershipfindfd doesn't support this
Complex time-based queriesfind-mmin, -newer, etc.
Act on results with -execfindBuilt-in, no pipe needed
Respect .gitignorefdAutomatic by default
Script portabilityfindAvailable on every system
CI/CD pipelinesfindNo extra installation
bash
# 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:

bash
# 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).

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:

bash
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:

bash
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 name
  • find . -mtime +30 -delete — time-based cleanup
  • find . -path "*/node_modules" -prune -o -name "*.ts" -print — skip directories for speed
  • find . -name "*.log" -exec gzip {} + — batch actions without piping
  • find . -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.