Where grep searches inside files, find searches for the files themselves — by name, size, age, ownership, permissions, or any combination. It also runs commands on whatever it finds, which is what turns it from a search tool into one of the most powerful things in the shell.

It has an unusual syntax that looks nothing like other Unix commands, which is the main reason people avoid it. Once the shape makes sense it is straightforward.

The shape of a find command

find [where to look] [what to match] [what to do]
find /var/log -name "*.log" -mtime +30 -delete
                                         └─ action
                             └────────────── test
               └────────────────────────── test
     └──────────────────────────────── starting directory

Everything after the path is a chain of tests that each file must pass, followed optionally by an action. With no action, find prints what it matched.

The simplest useful invocation, and the one you will type most:

find . -name "config.yml"

Matching by name

TestMatches
-name "*.conf"Filename, case-sensitive, glob pattern
-iname "*.CONF"Same but case-insensitive
-path "*/etc/*"The whole path rather than just the filename
-regex ".*\.(js|ts)$"Full path against a regular expression

Always quote the pattern. Without quotes the shell expands *.conf against your current directory before find ever runs, and you get either the wrong results or a confusing error. This is the number one find mistake.

Matching by type, size and time

find . -type f          # regular files only
find . -type d          # directories only
find . -type l          # symbolic links

-type f belongs in most commands you write. Without it you will match directories too, which usually is not what you meant.

find . -size +100M      # larger than 100 megabytes
find . -size -1k        # smaller than 1 kilobyte
find . -size 0          # exactly empty

Suffixes are c for bytes, k, M, G. With no suffix the unit is 512-byte blocks, which is rarely what anyone wants — always give a suffix.

TestMeaning
-mtime -7Content modified less than 7 days ago
-mtime +30Content modified more than 30 days ago
-mmin -60Modified in the last hour
-atimeLast accessed — often unreliable, many systems mount with noatime
-ctimeMetadata changed (permissions, ownership), not creation time
-newer FILEModified more recently than the given file

The + and - prefixes mean “more than” and “less than”, and a bare number means exactly that many days — which almost never matches anything you care about. Reach for + or - by default.

One caveat worth remembering: -ctime is change time, not creation time. Traditional Unix filesystems do not record creation time at all.

Matching by ownership and permissions

find /home -user kevin
find /srv -group developers
find . -perm 644                # exactly these permissions
find . -perm -u+w               # at least owner-writable
find / -perm /o+w -type f       # world-writable files — a security check
find / -perm -4000 -type f      # setuid binaries — worth auditing

The prefixes matter here too: no prefix means exact match, - means “all of these bits are set”, and / means “any of these bits are set”. See the file permissions page for what the numbers mean.

Combining tests

Tests listed one after another are joined with an implicit AND. For anything else you need explicit operators:

find . -name "*.jpg" -o -name "*.png"          # OR
find . -type f ! -name "*.txt"                 # NOT
find . \( -name "*.log" -o -name "*.tmp" \) -mtime +7   # grouping

The escaped parentheses are necessary because the shell would otherwise interpret them. This bites people constantly: without grouping, -mtime +7 in that last example would apply only to the *.tmp branch, because AND binds tighter than OR.

Doing something with the results

-exec

find . -name "*.log" -exec gzip {} \;
find . -name "*.log" -exec gzip {} +

{} is replaced by each filename. The terminator makes a real difference:

  • \; runs the command once per file. Ten thousand files means ten thousand process launches.
  • + batches as many filenames as fit onto one command line, like xargs. Dramatically faster.

Use + unless the command genuinely only accepts one argument at a time. Use -execdir instead of -exec when running commands in untrusted directories — it runs from within each file’s own directory and avoids a class of path-based attacks.

Piping to xargs

find . -name "*.py" -print0 | xargs -0 grep -l "import os"

Always pair -print0 with xargs -0. By default both split on whitespace, so a file called my report.txt becomes two arguments and everything breaks. The null-separated versions are the only safe form.

-delete

find /tmp/cache -type f -mtime +7 -delete

Run it without -delete first. Every time. Look at the list, confirm it is what you expect, then add the flag. There is no undo, and a misplaced test can match far more than you intended.

Note also that -delete implies -depth, which changes traversal order and can interact surprisingly with -prune.

Controlling where it looks

find . -maxdepth 1 -name "*.txt"        # this directory only, no recursion
find . -mindepth 2                      # skip the top level
find . -path "./node_modules" -prune -o -name "*.js" -print
find . -name "*.js" -not -path "*/node_modules/*"    # simpler, slower

-prune tells find not to descend into a directory at all, which is much faster than matching everything and filtering afterwards. The syntax is awkward — note the required -print at the end, because adding an explicit action disables the implicit one.

Also useful: -xdev stops find crossing onto other filesystems, which keeps a search of / from wandering into network mounts and /proc.

Recipes

# The ten largest files under a directory
find /var -type f -printf "%s %p\n" | sort -rn | head -10

# Delete empty directories, deepest first
find . -type d -empty -delete

# Files changed in the last day, most recent last
find . -type f -mtime -1 -printf "%T@ %p\n" | sort -n | cut -d' ' -f2-

# Fix permissions across a project
find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 644 {} +

# Find files containing a string (find locates, grep inspects)
find . -type f -name "*.conf" -exec grep -l "ServerName" {} +

# Count files by extension
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn

-printf is GNU-specific but very useful: %s is size, %p the path, %T@ the modification time as a number, %u the owner.

Gotchas

Order matters

find evaluates left to right and short-circuits. Putting cheap tests first is faster: -type f -name "*.log" beats -name "*.log" -type f on large trees. More importantly, -maxdepth must come before other tests or find warns that the result may not be what you expect.

Permission denied noise

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

Searching from / as a normal user produces pages of permission errors. Redirecting stderr silences them — but be aware you are also hiding real problems.

Symbolic links are not followed by default

Use -L before the path to follow them. Be careful: a symlink loop will make find run forever, though GNU find detects most cases.

locate is faster, when it is right

locate filename queries a prebuilt database and returns instantly, where find walks the disk. The trade-off is staleness — the database updates on a schedule, so anything created since the last run is invisible. Run sudo updatedb to refresh it. Use locate to find something you know exists somewhere; use find when the answer must be current or the criteria go beyond the name.

Quick reference

find . -name "*.txt"            # by name (quote it)
find . -iname "*.txt"           # case-insensitive
find . -type f                  # files only
find . -size +100M              # bigger than 100MB
find . -mtime -7                # changed in the last week
find . -empty                   # empty files and directories
find . -maxdepth 1              # no recursion
find . -name "*.log" -delete    # delete matches (list them first)
find . -name "*.c" -exec wc -l {} +      # run a command on matches
find . -print0 | xargs -0 cmd   # safe piping

Related commands

  • grep — searches file contents; find locates the files to search.
  • locate — instant name lookups from a cached database.
  • xargs — builds command lines from a list of inputs.
  • fd — a modern alternative with saner defaults and much shorter syntax. Not installed by default, but worth it.
  • du — when the question is really “what is using my disk space”.