The find command is one of the most powerful tools in the Unix toolkit — and one of the most underused. Most developers reach for it when they want to locate a file by name, but find can filter by size, age, permissions, and file type, and it can pipe results directly into other commands. Once you get comfortable with it, you’ll find yourself replacing a lot of ad-hoc scripting with a single well-crafted find invocation.

Basic Syntax

find [path] [expression]

The path is where to start searching (recursively). The expression is a combination of tests and actions.

$ find . -name "*.log"
./app/logs/access.log
./app/logs/error.log

Start with . to search the current directory tree, or give an absolute path like /var/log.

Searching by Name

-name is case-sensitive. Use -iname for a case-insensitive match.

$ find /etc -name "nginx.conf"
/etc/nginx/nginx.conf

$ find . -iname "readme*"
./README.md
./docs/readme.txt

Use shell wildcards (*, ?) inside quotes to prevent the shell from expanding them before find sees them.

Searching by Type

-type filters results by file type:

Flag Matches
-type f Regular files
-type d Directories
-type l Symbolic links
-type s Sockets
$ find . -type d -name "__pycache__"
./src/__pycache__
./tests/__pycache__

Searching by Size

-size accepts a number with a unit suffix:

Suffix Unit
c Bytes
k Kilobytes
M Megabytes
G Gigabytes

Prefix with + for “greater than” or - for “less than”:

$ find /var/log -type f -size +100M
/var/log/syslog.1

$ find . -type f -size -1k
./config/.gitkeep
./config/empty.conf

Searching by Modification Time

-mtime filters by last modification time in days. -mmin works the same way in minutes.

# Files modified in the last 24 hours
$ find . -type f -mtime -1

# Files not touched in over 30 days
$ find /tmp -type f -mtime +30

# Files modified in the last 60 minutes
$ find . -type f -mmin -60

There are three time flags:

  • -mtime — last modification time (content changed)
  • -atime — last access time (file was read)
  • -ctime — last status change time (permissions or ownership changed)

Searching by Permissions

-perm matches files by their permission bits:

# Files with exactly 644 permissions
$ find . -type f -perm 644

# Files that are world-writable (dangerous to have in web roots)
$ find /var/www -type f -perm -o+w

The - prefix means “at least these bits are set.” Without it, find requires an exact match.

Combining Tests with AND, OR, NOT

By default, multiple tests are ANDed together. Use -o for OR and ! for NOT:

# .log OR .tmp files
$ find . \( -name "*.log" -o -name "*.tmp" \) -type f

# Everything that is NOT a directory
$ find . ! -type d

Executing Commands with -exec

This is where find gets genuinely powerful. -exec runs a command for each matched file. Use {} as the placeholder for the filename and terminate the command with \;:

# Delete all .pyc files
$ find . -name "*.pyc" -exec rm {} \;

# Change ownership on all files in a web directory
$ find /var/www -type f -exec chown www-data:www-data {} \;

# Print the size of each matching file
$ find . -name "*.log" -exec du -sh {} \;
4.0K    ./app/logs/access.log
128M    ./app/logs/error.log

Replace \; with + to batch arguments into a single command invocation (faster for large result sets):

$ find . -name "*.pyc" -exec rm {} +

Using xargs for Better Performance

xargs is often more flexible than -exec ... +. Pipe find output into it:

$ find . -name "*.log" -print0 | xargs -0 grep "ERROR"

-print0 and -0 use the null byte as a delimiter, which handles filenames with spaces correctly.

Excluding Directories with -prune

-prune tells find to skip a directory entirely:

# Search everything except node_modules
$ find . -name "node_modules" -prune -o -name "*.js" -print

# Exclude multiple directories
$ find . \( -name ".git" -o -name "vendor" -o -name "node_modules" \) -prune -o -type f -print

The -o -print at the end is necessary — without it, -prune would suppress output for the non-pruned results.

Practical One-Liners

# Find and delete all empty directories
$ find . -type d -empty -delete

# Find duplicate filenames (same name, different location)
$ find . -type f -printf "%f\n" | sort | uniq -d

# List the 10 largest files under /var
$ find /var -type f -printf "%s %p\n" | sort -rn | head -10

# Find files owned by a specific user
$ find /home -user alice -type f

# Find SUID binaries (common security audit)
$ find / -perm -4000 -type f 2>/dev/null

Conclusion

find rewards learning its flags properly. The combination of type filtering, time-based searches, permission checks, and -exec makes it a complete filesystem query language. The key patterns to internalize are: use -print0 | xargs -0 for safe pipelining, use -prune to skip large directories, and always quote your wildcard patterns. Most tasks that feel like they need a shell loop can be handled more cleanly with a single find command.