Skip to content

Portal | Level: L1: Foundations | Topics: find, Bash / Shell Scripting | Domain: CLI Tools

find - Primer

Why This Matters

find is the Swiss Army knife of file discovery on Unix systems. Every operational task eventually requires locating files by name, age, size, ownership, or permission. When disk is filling up at 3 AM, when a security audit demands a list of every SUID binary, when you need to clean up six months of rotated logs, when you need to identify which config file changed in the last hour — find is the tool you reach for.

Unlike ls or shell globs, find descends recursively through directory trees, evaluates complex predicates, and can act on results in a single pipeline. It works on every POSIX system without installation. Knowing find deeply separates operators who fumble through directory listings from operators who solve problems in seconds.


Core Concepts

Basic Invocation

find [starting-path...] [expression]

The starting path defaults to . (current directory). The expression is a chain of tests (filters), actions (what to do with matches), and operators (combining logic).

# Find all files named "config.yaml" under /etc
find /etc -name "config.yaml"

# Find starting from multiple paths
find /var/log /tmp -name "*.log"

Path and Name Filters

The most common tests filter by name or path pattern.

# Case-sensitive name match (shell glob, not regex)
find /app -name "*.py"

# Case-insensitive name match
find /app -iname "*.py"

# Match the full path (useful for filtering by directory structure)
find /srv -path "*/config/*.yaml"

# Case-insensitive path match
find /srv -ipath "*/Config/*.YAML"

Critical detail: The -name pattern is matched against the filename only (basename), not the full path. Use -path or -wholename when you need to match directory components.

# This finds nothing — -name does not match path separators
find /app -name "src/*.py"

# This works — -path matches the full path
find /app -path "*/src/*.py"

Type Filters

# Regular files only
find /var -type f

# Directories only
find /home -type d

# Symbolic links only
find /usr/local -type l

# Block devices
find /dev -type b

# Character devices
find /dev -type c

# Named pipes (FIFOs)
find /tmp -type p

# Sockets
find /run -type s

Combining Predicates: -and, -or, -not

Predicates are implicitly ANDed. Use -o for OR, ! or -not for negation, and parentheses (escaped) for grouping.

# Files named *.log AND larger than 100M (implicit AND)
find /var/log -name "*.log" -size +100M

# Files named *.log OR *.gz
find /var/log \( -name "*.log" -o -name "*.gz" \)

# Files NOT named *.keep
find /tmp -not -name "*.keep"
# Equivalent:
find /tmp ! -name "*.keep"

# Complex: regular files that are (*.log OR *.txt) AND older than 7 days
find /data -type f \( -name "*.log" -o -name "*.txt" \) -mtime +7

Parentheses must be escaped or quoted to prevent shell interpretation: \( and \) or '(' and ')'.

Depth Control: -maxdepth and -mindepth

Control how deep find descends into the directory tree.

# Only the immediate directory (no recursion)
find /etc -maxdepth 1 -name "*.conf"

# Only items exactly 2 levels deep
find /opt -mindepth 2 -maxdepth 2 -type d

# Search everything but skip the top-level directory itself
find /var -mindepth 1 -type f

Performance note: Always place -maxdepth and -mindepth before other predicates. They are "global options" and GNU find will warn if they appear after tests.

Excluding Directories: -prune

Remember: The -prune pattern is always the same: find <path> <match-to-skip> -prune -o <real-match> -print. Think of it as "prune OR print" — if the directory matches the prune condition, skip it; otherwise, evaluate the real condition and print. Forgetting the -print at the end is the number one -prune mistake.

-prune tells find to skip descending into matched directories. It is one of the most misunderstood find features.

# Find all *.py files but skip .git directories
find /project -name ".git" -prune -o -name "*.py" -print

# Skip multiple directories
find /app \( -name node_modules -o -name .venv -o -name __pycache__ \) -prune -o -name "*.py" -print

# Skip a specific absolute path
find / -path /proc -prune -o -path /sys -prune -o -name "*.conf" -print

Key insight: -prune is an action (like -print). The pattern is:

find <path> <prune-condition> -prune -o <real-condition> -print

The -o (OR) is essential. Without -print at the end, find uses its default -print, which prints both the pruned directory names and the matched files.

Time-Based Filters

Modification Time (-mtime, -mmin)

# Files modified more than 30 days ago
find /var/log -type f -mtime +30

# Files modified in the last 24 hours
find /app -type f -mtime -1

# Files modified exactly 7 days ago (between 7*24h and 8*24h ago)
find /data -type f -mtime 7

# Files modified in the last 60 minutes
find /tmp -type f -mmin -60

# Files modified more than 120 minutes ago
find /tmp -type f -mmin +120

Time arithmetic: - -mtime +N means modified more than N24 hours ago - -mtime -N means modified less than N24 hours ago - -mtime N means modified between N24 and (N+1)24 hours ago

Access Time and Change Time

# Files accessed in the last hour
find /tmp -type f -amin -60

# Files whose inode changed in last 2 days (permissions, ownership, etc.)
find /etc -type f -ctime -2

Comparing Against a Reference File (-newer)

# Files modified more recently than /tmp/marker
find /app -newer /tmp/marker

# Create a reference timestamp and find files changed since
touch -t 202601150000 /tmp/ref
find /var/log -newer /tmp/ref -name "*.log"

# Files modified between two timestamps
touch -t 202601100000 /tmp/start
touch -t 202601150000 /tmp/end
find /data -newer /tmp/start ! -newer /tmp/end

Size Filters

# Files larger than 100 megabytes
find / -type f -size +100M

# Files smaller than 1 kilobyte
find /etc -type f -size -1k

# Files exactly 0 bytes (empty files)
find /tmp -type f -size 0

# Files between 10M and 100M
find /var -type f -size +10M -size -100M

Size suffixes: | Suffix | Meaning | |--------|---------| | c | bytes | | k | kibibytes (1024 bytes) | | M | mebibytes (1024^2) | | G | gibibytes (1024^3) |

Without a suffix, the unit is 512-byte blocks (legacy behavior).

Permission Filters (-perm)

-perm has three syntax modes that behave very differently.

# Exact match: permissions are exactly 0755
find /usr/local/bin -type f -perm 0755

# All-bits match: at least these bits are set (AND logic)
# Files where owner AND group AND other all have read
find /srv -type f -perm -0444

# Any-bit match: at least one of these bits is set (OR logic)
# Files that are world-writable OR group-writable
find /var -type f -perm /0022

# Symbolic mode works too
find /app -type f -perm -u+x    # owner has execute
find /srv -type f -perm /g+w    # group has write

Common confusion: - -perm 0644 — exactly 0644, nothing else - -perm -0644 — at least 0644 (could have more bits set) - -perm /0644 — any of the 0644 bits are set

User and Group Filters

# Files owned by a specific user
find /home -user jenkins

# Files owned by a specific UID (useful when user is deleted)
find / -uid 1001

# Files owned by a specific group
find /srv -group www-data

# Files with no valid user (orphaned after user deletion)
find / -nouser

# Files with no valid group
find / -nogroup

# Files owned by root that are world-writable (security concern)
find / -user root -perm /002 -type f

Empty Files and Directories

# Empty regular files (0 bytes)
find /tmp -type f -empty

# Empty directories
find /var -type d -empty

# Remove empty directories (careful!)
find /data/old -type d -empty -delete
# Find a file by inode number (useful when filename has special characters)
find / -inum 1234567

# Find all hard links to a specific file
find / -samefile /etc/passwd

# Find files with more than 1 hard link
find /data -type f -links +1

# Find files with exactly 1 hard link (no other links)
find /data -type f -links 1

When to use inode operations: - A filename contains unprintable characters or starts with - - You need to find all hard links to a file across mount points - You want to identify hard-linked duplicates consuming less space than expected

Regular Expression Matching (-regex)

# Match full path against a regex (default: emacs regex)
find /var/log -regex ".*\.\(log\|gz\)$"

# Use POSIX extended regex for cleaner syntax
find /var/log -regextype posix-extended -regex ".*\.(log|gz)$"

# Find files with timestamps in their names
find /backup -regextype posix-extended -regex ".*/[0-9]{4}-[0-9]{2}-[0-9]{2}\.tar\.gz"

Note: -regex matches against the entire path returned by find, not just the filename. The pattern must match from the beginning of the path.


Actions: What to Do with Matches

-print (default)

# These are equivalent:
find /tmp -name "*.log"
find /tmp -name "*.log" -print

-print0 (null-delimited output)

Filenames can contain spaces, newlines, quotes, and other special characters. -print0 separates results with null bytes (\0) instead of newlines, making downstream processing safe.

# Safe pipeline with xargs
find /data -name "*.csv" -print0 | xargs -0 wc -l

# Safe pipeline with while-read
find /data -name "*.csv" -print0 | while IFS= read -r -d '' file; do
  echo "Processing: $file"
done

-exec (run a command per file)

# Run chmod on each matching file (one process per file)
find /app -type f -name "*.sh" -exec chmod 755 {} \;

# Show detailed info for each match
find /etc -name "*.conf" -mtime -1 -exec ls -la {} \;

The {} is replaced with the current filename. The \; terminates the command (semicolon must be escaped).

-exec + (batch execution)

# Run chmod on all matching files at once (much faster)
find /app -type f -name "*.sh" -exec chmod 755 {} +

# Grep through all matching files in one invocation
find /src -name "*.py" -exec grep -l "import os" {} +

The + terminator causes find to batch filenames into as few command invocations as possible, similar to xargs. This is significantly faster than \; for large result sets.

-execdir (run command in the file's directory)

# Run the command in the directory containing the match
find /project -name "Makefile" -execdir make clean \;

-execdir changes to the directory containing the matched file before running the command. The {} is replaced with ./filename (no path prefix). This is safer and avoids path-length issues.

-delete (remove matching files)

# Delete old log files
find /var/log -name "*.log" -mtime +90 -delete

# Delete empty directories
find /tmp/build -type d -empty -delete

Warning: -delete implies -depth (processes children before parents). It cannot be undone. Always test with -print first.

Gotcha: Because -delete implies -depth, you cannot combine -delete with -prune. The -depth flag processes directory contents before the directory itself, but -prune needs to see the directory first to skip its contents. If you need to prune and delete, use -exec rm instead.

Piping to xargs

# Safe: null-delimited
find /data -name "*.json" -print0 | xargs -0 -I {} cp {} /backup/

# Parallel execution (4 processes at a time)
find /images -name "*.png" -print0 | xargs -0 -P 4 -I {} convert {} -resize 800x600 {}.resized.png

# Limit batch size (100 files per invocation)
find /logs -name "*.log" -print0 | xargs -0 -n 100 gzip

-printf (custom output format)

# Print filename and size in bytes
find /var -type f -printf "%p\t%s\n"

# Print modification time and path (sortable)
find /app -type f -printf "%T@ %p\n" | sort -n

# Print permissions, owner, size, and path
find /srv -type f -printf "%m %u %s %p\n"

Key format specifiers: %p (full path), %f (basename), %h (dirname), %s (size bytes), %m (perms octal), %M (perms symbolic), %u (owner), %g (group), %T@ (mtime epoch), %Tc (mtime human), %l (symlink target), %i (inode), %n (hard link count).


Production Examples

Disk Space Triage

# Top 20 largest files on the system
find / -xdev -type f -printf "%s %p\n" 2>/dev/null | sort -rn | head -20

# Files larger than 1G, sorted by size
find / -xdev -type f -size +1G -printf "%s\t%p\n" 2>/dev/null | sort -rn

# Large files modified in the last day (likely culprits for sudden disk growth)
find / -xdev -type f -size +100M -mtime -1 -printf "%s\t%T+\t%p\n" 2>/dev/null | sort -rn

The -xdev flag prevents find from crossing filesystem boundaries (stays on the same mount point).

Security Audit

# Find all SUID binaries
find / -type f -perm -4000 -ls 2>/dev/null

# Find all SGID binaries
find / -type f -perm -2000 -ls 2>/dev/null

# World-writable files (excluding /proc, /sys, /dev)
find / -path /proc -prune -o -path /sys -prune -o -path /dev -prune -o \
  -type f -perm /002 -print 2>/dev/null

# World-writable directories without sticky bit
find / -type d -perm /002 ! -perm -1000 2>/dev/null

# Files with no owner
find / -nouser -o -nogroup 2>/dev/null

Log Cleanup Automation

# Compress logs older than 7 days
find /var/log/app -name "*.log" -mtime +7 -exec gzip {} +

# Remove compressed logs older than 90 days
find /var/log/app -name "*.log.gz" -mtime +90 -delete

# Remove empty log files
find /var/log -name "*.log" -type f -empty -delete

# Rotate by size: compress any log file over 100M
find /var/log -name "*.log" -size +100M -exec gzip {} +

Deployment Cleanup

# Find and remove Python bytecode
find /app -type f -name "*.pyc" -delete
find /app -type d -name "__pycache__" -exec rm -rf {} +

# Remove node_modules from all projects
find /home/deploy -maxdepth 3 -name "node_modules" -type d -prune -exec rm -rf {} +

# Find stale Docker build contexts
find /tmp -maxdepth 1 -name "docker-build-*" -type d -mtime +1 -exec rm -rf {} +

Configuration Drift Detection

# Find config files changed in the last hour
find /etc -type f \( -name "*.conf" -o -name "*.cfg" -o -name "*.yaml" \) -mmin -60

# Find config files not owned by root
find /etc -type f ! -user root -ls
# Find broken symlinks
find /usr/local -type l ! -exec test -e {} \; -print

# Find symlinks pointing to a specific target
find /etc -type l -lname "*/nginx*"

Quick Reference: Common Flags

Flag Purpose Example
-name Match filename (glob) -name "*.log"
-iname Case-insensitive name -iname "readme*"
-path Match full path (glob) -path "*/src/*.py"
-type f Regular files -type f
-type d Directories -type d
-type l Symbolic links -type l
-size +N Larger than N -size +100M
-size -N Smaller than N -size -1k
-mtime +N Modified > N days ago -mtime +30
-mtime -N Modified < N days ago -mtime -1
-mmin +N Modified > N minutes ago -mmin +60
-newer FILE Modified after FILE -newer /tmp/ref
-user NAME Owned by user -user www-data
-group NAME Owned by group -group docker
-nouser No valid owner -nouser
-perm MODE Exact permissions -perm 0644
-perm -MODE At least these bits -perm -0755
-perm /MODE Any of these bits -perm /0022
-empty Zero-size files or empty dirs -empty
-maxdepth N Limit descent depth -maxdepth 2
-mindepth N Skip shallow levels -mindepth 1
-prune Do not descend into match -name .git -prune
-xdev Stay on same filesystem -xdev
-exec CMD \; Run CMD per file -exec ls -la {} \;
-exec CMD + Run CMD with batched files -exec chmod 644 {} +
-execdir CMD Run CMD in file's directory -execdir make \;
-delete Remove matching files -delete
-print0 Null-delimited output -print0
-printf FMT Custom output format -printf "%s %p\n"
-ls Detailed listing (like ls -l) -ls
-regex Full-path regex match -regex ".*\.log$"
-inum N Match inode number -inum 1234567
-samefile F Same inode as F -samefile /etc/hosts
-links +N More than N hard links -links +1
-not / ! Negate test ! -name "*.bak"
-o OR -name "*.log" -o -name "*.gz"
-L Follow symlinks find -L /app

Performance Considerations

Use -maxdepth When You Can

If you know the target files live at a certain depth, constrain the search. Searching / without -maxdepth on a server with millions of files takes minutes.

Prefer -exec + Over -exec \;

Batching with + drastically reduces process spawning overhead.

# Bad: 10,000 files = 10,000 grep processes
find /src -name "*.java" -exec grep -l "Pattern" {} \;

# Good: 10,000 files = maybe 5 grep processes
find /src -name "*.java" -exec grep -l "Pattern" {} +

Use -xdev to Stay on One Filesystem

Prevents find from wandering into /proc, /sys, NFS mounts, or other slow/virtual filesystems.

find / -xdev -type f -size +500M

Redirect stderr

find on / produces many "Permission denied" errors. Redirect them:

find / -name "*.conf" 2>/dev/null

Place Tests Before Actions

find evaluates left to right and short-circuits on AND. Put cheap tests (like -name) before expensive ones (like -exec) so fewer files hit the expensive path.


Portability Notes

Feature GNU find BSD/macOS find POSIX
-maxdepth Yes Yes No (but widely supported)
-printf Yes No No
-print0 Yes Yes No (but widely supported)
-regex Yes Yes (different default type) No
-perm /mode Yes No (uses -perm +mode) No
-delete Yes Yes No
-exec {} + Yes Yes Yes (POSIX 2008)
-execdir Yes Yes No
-samefile Yes No No

For maximum portability, stick to -name, -type, -size, -mtime, -perm MODE, -exec {} \;, -user, -group, and -print.


Wiki Navigation

Prerequisites