Mastering the find command: search and automate in Unix-like systems

find is the standard utility for searching files and directories on Unix-like systems. It’s extremely powerful: filter by name, type, size, permissions, dates, contents, and even run commands on each result.

Basic syntax

find [PATHS...] [OPTIONS] [TESTS ...] [ACTIONS]
  • PATHS: one or more starting points (default: .).
  • TESTS: filter conditions (e.g., -name, -type, -size).
  • ACTIONS: what to do with the results (e.g., -print, -exec, -delete).

Quick examples

All .log files in the current directory (recursive):

find . -type f -name "*.log"

Case-insensitive:

find . -type f -iname "*.jpg"

Only in the current folder, without descending into subdirectories:

find . -maxdepth 1 -type f -name "*.sh"

Common filters

By name and path

find /var/log -type f -name "syslog*"
find ~ -type f -path "*/node_modules/*"       # matches full path
find . -regex '.*\.\(jpg\|png\|gif\)$'        # Emacs-style regex

By type

find /etc -type f   # regular files
find /etc -type d   # directories
find / -type l      # symlinks
find . -type s      # sockets
find . -type p      # named pipes

By size

-size units: c=bytes, k=KiB, M=MiB, G=GiB. Prefixes: - “less than”, + “more than”.

find . -type f -size +100M       # >100 MiB
find . -type f -size -10k        # <10 KiB
find . -type f -size 0           # exactly 0 bytes

By time

-mtime days, -mmin minutes (last modification); similar: -atime/-amin (access), -ctime/-cmin (metadata change).

find . -type f -mtime -1         # modified in the last 24h
find . -type f -mmin -30         # modified in the last 30 minutes
find /var/log -type f -mtime +7  # older than 7 days

Permissions, ownership, empties

find . -empty                 # empty files or dirs
find . -user alice            # owner
find . -group www-data        # group
find . -perm 0644             # exact permissions (octal)
find . -perm -0100            # contains execute bit for owner

Combining conditions

Logical operators: -a/-and (AND), -o/-or (OR), ! (NOT). Use parentheses for precedence; escape them in shell.

find . \( -name "*.png" -o -name "*.jpg" \) -type f ! -size 0

Excluding paths (pruning)

-prune prevents descending into certain directories. Typical pattern to skip node_modules and .git:

find . -type d \( -name node_modules -o -name .git \) -prune -o -type f -print

Follow symlinks

find -L /path -type f -name "*.conf"   # -L follows symbolic links

Printing results

-print is implicit, but you can customize output with -printf (GNU find):

find . -type f -printf '%p %s bytes %TY-%Tm-%Td %TH:%TM\n'

Running commands on results

-exec with \; vs +

  • -exec ... \; runs the command once per file.
  • -exec ... + batches multiple files into one execution (more efficient).
# Remove .tmp files (one call per file)
find . -type f -name "*.tmp" -exec rm -v {} \;

# Compress in batches (fewer calls)

find . -type f -name "\*.log" -mtime +7 -exec gzip -9 {} +

-ok: interactive confirmation

find . -type f -name "*.bak" -ok rm {} \;

-print0 and xargs -0 (safe with spaces)

Use NUL terminators to handle names with spaces or newlines.

find . -type f -name "*.txt" -print0 | xargs -0 wc -l

Deleting safely

  • Test first with -print to verify matches.
  • Prefer -delete over -exec rm when possible; often safer and faster.
# DRY RUN
find . -type f -name "*.tmp" -print

# Actually delete

find . -type f -name "\*.tmp" -delete

To avoid unexpected removal of non-empty directories, some versions require -depth when combining -delete with directory patterns:

find . -depth -type d -name "__pycache__" -delete

Practical recipes

Files larger than 1 GiB, show “human” size:

find . -type f -size +1G -print0 | xargs -0 du -h

Search executable scripts with shebang:

find . -type f -perm -0100 -exec head -n1 {} \; | grep -n "^#!"

Find files modified today and open in editor:

find . -type f -daystart -mtime 0 -exec "$EDITOR" {} +

Exclude build folders and search C/C++ sources:

find . -type d \( -name build -o -name dist \) -prune -o \
  -type f \( -name "*.c" -o -name "*.cpp" -o -name "*.h" \) -print

Find broken symlinks:

find . -xtype l

Clean up temporary files left by editors:

find . -type f \( -name "*~" -o -name ".*.swp" -o -name ".DS_Store" \) -delete

Order, depth, and performance

  • -maxdepth N limits traversal; -mindepth N skips initial levels.
  • -mount/-xdev avoids crossing into other filesystems (useful on /).
  • -samefile, -inum, and -links help with hard links and inodes.
# Search only on the current filesystem
find / -xdev -type f -size +1G -print

Advanced formatting with -printf (GNU)

Specify fields as in printf(3):

  • %p=path, %f=name, %h=dir, %s=bytes
  • %u=user, %g=group, %m=permissions, %TY/%Tm/%Td=date
find . -type f -printf '%m %u %g %s %p\n' | sort -k4,4n

Common pitfalls

  • Shell glob vs find: quote patterns ("*.txt") to avoid shell expansion.
  • Spaces in names: use -print0 with xargs -0 or -exec ... {} +.
  • Parentheses: remember backslashes: \( ... \).
  • Compatibility: options like -printf are GNU; on macOS you may need findutils (e.g., gfind).

Minimal cheat sheet

# Names
find . -name "*.ext"                # case-sensitive
find . -iname "*.ext"               # case-insensitive

# Type

find . -type f|d|l

# Size

find . -size +100M                  # >100 MiB

# Time

find . -mtime -1                    # modified < 1 day

# Depth

find . -maxdepth 2 -mindepth 1

# Exclusions

find . -path "*/.git/*" -prune -o -print

# Actions

find . -exec cmd {} ;              # per-file
find . -exec cmd {} +               # batch
find . -delete                      # delete (careful!)

# Name safety

find . -print0 | xargs -0 cmd

Conclusion

find offers a rich grammar for querying the filesystem and automating workflows. Start with simple patterns, verify with -print, then add -exec or -delete cautiously. With just a few well-chosen options, you can replace complex scripts and save time every day.

Back to top