Every developer knows find . -name "*.txt". Few know that find can replace half your shell scripts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Find by name (case-insensitive)
find . -iname "readme*"

# Find by extension
find . -name "*.py"

# Find by exact name
find . -name "Makefile"

# Find excluding directories
find . -name "*.js" -not -path "./node_modules/*"

The -not (or !) operator is your friend for excluding noise.

Filter by Type

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Files only
find . -type f

# Directories only
find . -type d

# Symbolic links
find . -type l

# Empty files
find . -type f -empty

# Empty directories
find . -type d -empty

Filter by Time

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Modified in last 24 hours
find . -mtime -1

# Modified more than 7 days ago
find . -mtime +7

# Modified exactly 7 days ago
find . -mtime 7

# Accessed in last hour (minutes)
find . -amin -60

# Changed (metadata) in last day
find . -ctime -1

Time units: -mmin (minutes), -mtime (days). The +/- prefix means “more than”/“less than”.

Filter by Size

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Larger than 100MB
find . -size +100M

# Smaller than 1KB
find . -size -1k

# Exactly 0 bytes
find . -size 0

# Between 1MB and 100MB
find . -size +1M -size -100M

Units: c (bytes), k (KB), M (MB), G (GB).

Filter by Permissions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# World-writable files (security audit)
find . -perm -002 -type f

# Executable files
find . -perm /u+x -type f

# SUID files (security concern)
find / -perm -4000 -type f 2>/dev/null

# Files with specific permissions
find . -perm 644

Filter by Owner

1
2
3
4
5
6
7
8
# Files owned by user
find . -user deploy

# Files owned by group
find . -group www-data

# Files with no owner (orphaned)
find . -nouser

Combining Filters

Use -and (default), -or, and parentheses:

1
2
3
4
5
6
7
8
# Python or JavaScript files
find . \( -name "*.py" -or -name "*.js" \)

# Large, old log files
find /var/log -name "*.log" -size +10M -mtime +30

# Config files modified recently
find /etc -name "*.conf" -type f -mtime -1

Taking Action

-exec: Run Command on Each Result

1
2
3
4
5
6
7
8
# Delete old temp files
find /tmp -name "*.tmp" -mtime +7 -exec rm {} \;

# Change permissions
find . -type f -name "*.sh" -exec chmod +x {} \;

# Show file details
find . -name "*.log" -exec ls -lh {} \;

The {} is replaced with each filename. \; terminates the command.

-exec with + : Batch Execution

1
2
3
4
# Much faster - batches arguments
find . -name "*.txt" -exec grep -l "TODO" {} +

# Compare: \; runs grep once per file, + runs grep once with all files

Use + when the command accepts multiple arguments (like grep, rm, chmod).

-delete: Built-in Removal

1
2
3
4
5
# Delete empty directories
find . -type d -empty -delete

# Delete old files (CAREFUL!)
find /tmp -mtime +30 -delete

-delete implies -depth (processes contents before directory).

-print0 and xargs

For complex operations or filenames with spaces:

1
2
3
4
5
# Safe deletion with xargs
find . -name "*.bak" -print0 | xargs -0 rm

# Parallel processing
find . -name "*.jpg" -print0 | xargs -0 -P 4 mogrify -resize 50%

Practical Patterns

Cleanup Old Files

1
2
3
4
5
# Delete logs older than 30 days
find /var/log -name "*.log" -mtime +30 -delete

# Archive then delete
find /data -name "*.csv" -mtime +90 -exec gzip {} \; -exec mv {}.gz /archive/ \;

Find and Replace in Files

1
2
3
# Find files containing pattern, then replace
find . -name "*.py" -exec grep -l "old_function" {} \; | \
    xargs sed -i 's/old_function/new_function/g'

Disk Usage Investigation

1
2
3
4
5
# Top 10 largest files
find . -type f -exec ls -s {} + | sort -n -r | head -10

# Large files by type
find . -name "*.log" -size +100M -exec ls -lh {} \;

Security Auditing

1
2
3
4
5
6
7
8
# World-writable files
find /var/www -perm -002 -type f

# Files modified in last hour (intrusion detection)
find /var/www -mmin -60 -type f

# SUID/SGID binaries
find / -perm /6000 -type f 2>/dev/null

Code Quality

1
2
3
4
5
6
7
8
# Find TODOs in codebase
find . -name "*.py" -exec grep -Hn "TODO\|FIXME" {} \;

# Large functions (files over N lines)
find . -name "*.py" -exec awk 'END{if(NR>500)print FILENAME": "NR" lines"}' {} \;

# Files without copyright headers
find . -name "*.py" -exec sh -c 'head -5 "$1" | grep -q "Copyright" || echo "$1"' _ {} \;

Sync and Backup

1
2
3
4
5
# Find files newer than reference
find . -newer /tmp/last_backup -type f

# Copy structure preserving paths
find . -name "*.conf" -exec cp --parents {} /backup/ \;

Performance Tips

Limit Depth

1
2
3
4
5
# Only current directory
find . -maxdepth 1 -name "*.txt"

# Top two levels
find . -maxdepth 2 -type d

Prune Directories

1
2
3
4
5
# Skip .git directories entirely
find . -path "./.git" -prune -o -name "*.py" -print

# Skip multiple directories
find . \( -path ./node_modules -o -path ./.git \) -prune -o -type f -print

-prune stops descending into matched directories — much faster than -not -path.

Use -quit for First Match

1
2
# Stop after finding one
find . -name "config.yaml" -quit

Common Mistakes

Forgetting quotes around patterns:

1
2
3
4
5
# Bad - shell expands * before find sees it
find . -name *.txt

# Good
find . -name "*.txt"

Wrong order with -delete:

1
2
3
4
5
# DANGEROUS - deletes everything, then filters
find . -delete -name "*.tmp"

# Safe - filter first
find . -name "*.tmp" -delete

Inefficient -exec:

1
2
3
4
5
# Slow - one grep per file
find . -name "*.log" -exec grep "ERROR" {} \;

# Fast - batch
find . -name "*.log" -exec grep "ERROR" {} +

Quick Reference

FlagPurpose
-nameMatch filename pattern
-inameCase-insensitive name
-type f/d/lFiles/directories/links
-mtime +/-NModified N days ago
-size +/-NSize threshold
-permPermission match
-exec cmd {} \;Run command per file
-exec cmd {} +Batch command execution
-print0Null-delimited output
-deleteRemove matches
-pruneSkip directory descent
-maxdepth NLimit search depth

find is a complete file-processing language. Learn it well, and you’ll write fewer scripts.


Computing Arts is CLI mastery for working developers. More at computingarts.com.