Skip to main content

Linux command: find

14 July 2026

You know a file is somewhere on the server. Maybe it is a config changed last night, a giant log filling the disk, or every .jpg buried three folders deep. Clicking through directories would take an hour. One command walks the entire tree for you and finds them all in seconds, by name, by size, by age, by anything: find.

1. The Basics

The job of find is to walk through a directory and everything inside it, look at each file and folder one by one, and act on the ones that match your description. That description can be a name, a size, an age, a type, a permission, or several of those combined.

The basic shape of the command is always the same: the word find, a starting place to search, and then a test that says what you are looking for.

$ find /etc -name "hosts"
/etc/hosts

Here /etc is where the search begins, and -name "hosts" is the test. find descends through /etc and all its subfolders, and prints the full path of every entry named hosts.

Two things make find different from a normal search box. First, it searches by metadata, not content: it works from the name, size, timestamp, owner, and type that the filesystem records, so it never needs to open a file to decide. (For searching inside files, that is grep's job.) Second, it can act on what it finds: it does not only print paths, it can delete them, copy them, change their permissions, or run any command you like on each one.

find does not just locate files; it walks a tree, tests every entry against a description, and then does something with the ones that pass.

The right mental model: find is not a filter, it is an expression evaluator that walks a tree. For every file it meets, it works through your tests from left to right and asks "does this one match?" Everything else, the dozens of options, is just a richer way to describe the match and to say what happens when it succeeds.

Back to top

2. Where the Name Comes From

Unlike its cryptic neighbours grep, awk, and sed, the name find is not an acronym or an in-joke. It is simply the plain English verb. The early Unix authors, who loved short names, could not make this one much shorter, so they left it as the ordinary word for the ordinary task: to find things.

There is a small twist worth knowing, though. The name suggests the tool only locates files, but that undersells it. From its earliest days find could also run a command on each match. So a better way to read the name is "find each file that matches, and then do this to it". That second half is where most of its power lives.

Read the command as a sentence: find WHERE WHAT-TO-MATCH WHAT-TO-DO. Start here, match files like this, then act on them like that. Once you see that shape, every find command becomes easy to read.

Back to top

3. A Short History

find is one of the oldest tools in Unix. It appeared in Version 5 Unix around 1974 at Bell Labs, in the same early era that produced grep and the shell. From the start it combined two ideas that were unusual for the time: it walked a directory tree recursively, and it let you attach an action (-exec) to run on each file it found. That pairing made it a small automation engine, not just a search tool.

As Unix spread, every vendor shipped its own find, and the tests drifted apart. POSIX later standardised a common core so that a basic find command works the same everywhere. On Linux, the version you actually run is GNU findutils, which adds many conveniences on top of the POSIX base, such as -maxdepth, -delete, and the safe -print0 output.

EraMilestone
~1974 find appears in Version 5 Unix, already with recursive walking and -exec
1980s Every Unix vendor ships its own find; the tests slowly diverge
1990s POSIX standardises a common core; GNU findutils becomes the Linux version
GNU era GNU adds -print0 and xargs -0 to handle filenames with spaces and newlines safely
Today GNU find is the default on Linux; faster reimplementations like fd and bfs exist

The GNU -print0 idea is worth singling out. Filenames on Linux may contain spaces and almost any character, which used to break scripts that split output on spaces. GNU solved it by letting find separate results with an invisible null byte, which no filename can contain. That is why find ... -print0 | xargs -0 became the standard safe pattern, and it started here.

The BSD find that ships with macOS is close but not identical. Most everyday commands work the same; the differences show up in less common options, which the sections below point out where they matter.

Back to top

4. Simple Use Cases

4.1 The Simplest Possible Use

Give find a starting directory and nothing else. It prints every file and folder underneath it, one full path per line:

$ find /etc/ssh
/etc/ssh
/etc/ssh/sshd_config
/etc/ssh/ssh_config
/etc/ssh/ssh_host_rsa_key

On GNU find you can even leave out the path, and it searches the current directory . by default:

$ find
.
./notes.txt
./sub
./sub/report.pdf

This bare listing is already useful, but the point of find is to narrow it with a test.

4.2 Find by Name

The -name test matches the file's name against a pattern. The pattern uses shell wildcards (* for any characters, ? for one), not regular expressions. Always quote the pattern so the shell hands it to find untouched:

$ find /var/www -name "*.php"
/var/www/index.php
/var/www/admin/login.php

To ignore case, use -iname (short for insensitive name). This is the version to reach for when you are not sure how a file was capitalised:

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

4.3 Find by Type

The -type test limits results to one kind of entry. The two you will use most are f for a regular file and d for a directory:

$ find /var/log -type f -name "*.log"
/var/log/syslog
/var/log/nginx/access.log

Here -type f makes sure a folder that happens to be named something.log is not included. The full list of type letters:

LetterType
f Regular file
d Directory
l Symbolic link
b / c Block or character device
p / s Named pipe (FIFO) or socket

By default find descends the whole tree, however deep it goes. The -maxdepth option (short for maximum depth) stops it after a set number of levels. The starting directory is depth 0, its direct contents are depth 1, and so on:

$ find . -maxdepth 1 -type f
./notes.txt
./config.yml

This lists files in the current folder only, without diving into subfolders. Its partner -mindepth (short for minimum depth) does the opposite and skips the shallow levels. Put these options before the tests; GNU find warns if they come later.

Back to top

5. Moderate Use Cases

5.1 Find by Size

The -size test finds files by how big they are, which is how you hunt down whatever is filling a disk. Add a suffix for the unit: c for bytes, k for kilobytes, M for megabytes, and G for gigabytes. A leading + means "larger than" and a leading - means "smaller than":

$ find /var -type f -size +100M
/var/log/journal/system.journal
/var/backups/db-dump.sql

This prints every file over 100 megabytes. Combined with a sort, it becomes a quick "what is eating my disk" report, which the pipelines in section 6 build on.

5.2 Find by Time

This is one of the most useful and most misunderstood parts of find. The -mtime test (short for modification time) matches by the day a file's contents last changed, counted in 24-hour blocks. The number works like size: -mtime -7 means "changed less than 7 days ago", +7 means "more than 7 days ago", and a plain 7 means "exactly 7 days ago".

$ find /etc -mtime -1
/etc/hosts
/etc/passwd

This finds the config files changed in the last day, which is exactly what you want after "something changed and now it is broken". For finer control, -mmin (short for modification minutes) uses minutes instead of days:

$ find /tmp -mmin -30 -type f     # files touched in the last half hour

There are two related tests: -atime for when a file was last read (access time) and -ctime for when its metadata last changed (change time, such as a rename or permission edit).

5.3 Combining Tests

When you write several tests next to each other, find joins them with an invisible "and". Every test must pass for a file to match:

$ find /var/www -type f -name "*.log" -size +10M
/var/www/app/storage/laravel.log

To say "or" instead, use -o (short for or). Because "and" binds tighter than "or", wrap the alternatives in escaped parentheses so the grouping is clear:

$ find . -type f \( -name "*.jpg" -o -name "*.png" \)
./logo.png
./photos/beach.jpg

The backslashes stop the shell from treating the parentheses as its own syntax. You can also negate a test with ! or the more readable -not:

$ find . -type f -not -name "*.txt"     # every file that is NOT a .txt

5.4 Other Common Tests

A handful of tests cover most remaining daily needs:

TestFinds
-empty Empty files and empty directories
-user peter Files owned by user peter
-group www-data Files belonging to a group
-newer ref.txt Files modified more recently than ref.txt
-path "*/cache/*" Match against the whole path, not just the name

5.5 Find by Permission

The -perm test finds files by their permission bits, but it has three modes, and mixing them up is a classic source of "why did that match nothing?". The difference is one leading character:

FormMeaning
-perm 644 Exactly these bits, no more and no less
-perm -0002 All of these bits are set (other bits may be too)
-perm /6000 Any of these bits is set

For security checks you almost always want the - form, because you care whether a bit is set, not whether the whole mode matches exactly. This is how you audit the two things worth watching on a server, world-writable files and SUID programs:

$ find /var/www -type f -perm -0002     # world-writable: anyone can edit these
/var/www/uploads/old.php

$ find /usr/bin -perm -4000             # SUID: these run as their owner, often root
/usr/bin/passwd
/usr/bin/sudo

A plain -perm 777 would only match files whose bits are exactly rwxrwxrwx, so it misses a world-writable file that also has, say, the SUID bit set. The -0002 form catches every file the world can write, which is what you actually meant.

Back to top

6. Advanced Use Cases

6.1 Running a Command on Each Match with -exec

The real power of find is -exec, which runs a command on every file it finds. Inside the command, the placeholder {} stands for the current file, and the command ends with an escaped semicolon \;:

$ find . -name "*.tmp" -exec rm {} \;

This runs rm once for every .tmp file. That works, but starting a new process per file is slow when there are thousands of them. The + form fixes this: it collects as many files as fit and passes them to a single command, exactly like xargs does:

$ find . -name "*.tmp" -exec rm {} +     # one rm for many files, much faster

Use \; when the command must see one file at a time, and + when the command accepts a list and you want speed. There is also -execdir, which runs the command from inside each file's own directory. That is safer against tricks played by odd path names, and it is the better choice in scripts.

6.2 Confirm Before Acting, and Delete Safely

For dangerous actions, -ok works like -exec but asks you to confirm each file before it runs:

$ find . -name "*.bak" -ok rm {} \;
< rm ... ./old.bak > ?

GNU find also has a built-in -delete that removes matches without spawning rm at all. It is convenient but unforgiving, so build the command as a search first, look at the output, and only then swap -print for -delete:

$ find . -name "*.tmp" -print      # LOOK first
$ find . -name "*.tmp" -delete     # then delete, once you trust the list

6.3 The Safe Pipe: -print0 and xargs -0

Piping find into another tool is common, but a plain pipe splits on spaces and newlines, so a file named my report.pdf breaks into two. The fix is the null-separated pair: -print0 ends each result with an invisible null byte instead of a newline, and xargs -0 reads that same separator:

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

No filename can contain a null byte, so this handles spaces, quotes, and newlines without a single surprise. Make it your default whenever find feeds another command through a pipe.

6.4 Pruning: Skipping Whole Branches

Sometimes you want find to avoid an entire folder, such as .git or node_modules, so it does not waste time walking into it. The -prune action tells find "do not descend into this one". The pattern reads oddly at first but is worth memorising:

$ find . -path "*/node_modules" -prune -o -name "*.js" -print
./src/app.js
./src/util.js

Read it as: if the path is a node_modules folder, prune it (skip its contents); otherwise, if the name ends in .js, print it. The explicit -print at the end is required here, because once you use an action like -prune, find no longer prints automatically.

6.5 Matching with Real Regular Expressions

If shell wildcards are not enough, -regex matches the whole path against a regular expression. Note that it tests the entire path, not just the name, so remember to allow for the leading directories:

$ find . -regextype posix-extended -regex ".*/[0-9]{4}-[0-9]{2}-[0-9]{2}\.log"
./logs/2026-07-11.log

The -regextype option chooses the dialect; posix-extended behaves like grep -E. This is a GNU feature, so avoid it in scripts that must also run on macOS or BSD.

By default find looks at a symbolic link itself, not the file it points to, and it never walks through a link into another folder. Three options, placed before the path, change this:

OptionBehaviour
-P Never follow symlinks (the default)
-L Always follow symlinks and search what they point to
-H Follow only the links you named on the command line, not ones met while walking
$ find -L /var/www -name "*.php"     # follow links into shared folders too

Because -type l matches a link by looking at the link itself, it cannot tell whether the target still exists. For that, use -xtype l (short for the type after following the link), which reports links whose target is missing. It is the quick way to hunt down broken symlinks after files were moved or deleted:

$ find /var/www -xtype l
/var/www/current
/var/www/logs/latest.log
Back to top

7. Something Most Users Do Not Know

7.1 The Order of Tests Is the Speed of find

find evaluates your tests strictly from left to right and stops as soon as the answer is decided, exactly like && in a shell. This means the order you write tests in changes how fast the command runs. A cheap test, like -name, only reads the directory listing. An expensive test, like -size or anything with -exec, has to look at the file itself. Put the cheap test first:

$ find . -name "*.log" -size +10M     # fast: only stats files already named *.log
$ find . -size +10M -name "*.log"     # slower: stats every file, then checks the name

Both give the same result, but the first version only inspects the size of files that already passed the name test. On a large tree the difference is real.

A find command is a chain of yes/no questions asked in order. Ask the cheapest, most selective question first, and find spends its effort only on the files that survive it.

7.2 Every Test Is Secretly an Expression

What looks like a list of options is actually a small boolean language. The implicit "and" between tests is really the operator -a; -o is "or"; ! is "not"; and -print, -delete, and -exec are not settings but actions that return true or false and have a side effect. This is why find . -name "*.log" prints anything at all: when you write no action, find silently adds -print for you. The moment you add your own action, that automatic -print disappears, which is the reason the prune example in section 6 needed an explicit -print. Once you see tests and actions as one expression, the trickier commands stop being magic.

7.3 What find Actually Reads: the Inode

When section 1 said find works from metadata and never opens a file, this is the machinery behind it. Every file on a Linux filesystem is described by an inode, a small record that holds the owner, the permission bits, the three timestamps, the size, the link count, and pointers to the data blocks, but not the name and not the contents. As find walks a directory it calls readdir() to list the names, then stat() or lstat() to read each inode. Tests like -size, -mtime, -perm, and -user are just questions asked of that inode record, which is why they are cheap and why find never has to read a single byte of the file's data.

This also explains the speed rule from earlier: a -name test needs only the directory listing, while -size forces a stat() on the inode. Putting the name test first lets find skip the inode lookups it does not need.

7.4 Knowing Where find Stops

Part of expertise is knowing which tool takes over when find reaches its edge:

NeedUseWhy
Search inside file contents grep find matches names and metadata, not what is written in the file
Look up one file instantly by name locate Reads a prebuilt index, so it answers in an instant but can be out of date
See a folder's shape at a glance tree Draws the hierarchy visually instead of one path per line
The same search, but faster and simpler fd / bfs Modern reimplementations with friendlier syntax and parallel walking

A common and powerful team is find plus grep: use find to select which files by name, type, or age, then hand them to grep to search inside them. That is what find . -name "*.conf" -exec grep -l "debug" {} + does.

Back to top

8. Best Practices

  • Always quote name patterns. Write -name "*.log", not -name *.log, or the shell expands the * before find ever sees it.
  • Put the cheapest test first. Lead with -name or -type so expensive tests and -exec only run on files that already matched.
  • Preview before you act. Build the command with -print, check the list, and only then change it to -delete or -exec rm.
  • Prefer -exec ... + over -exec ... \; when the command accepts many files. It runs far fewer processes and is much faster.
  • Use -print0 | xargs -0 in pipes. It is the only way to handle filenames with spaces or newlines safely.
  • Limit the depth on big trees. -maxdepth and -prune keep a search from wandering into .git, node_modules, or a mounted network drive.
  • Use -quit to stop at the first hit. When you only need to know whether any match exists, -quit makes find exit as soon as it finds one, instead of walking the whole tree.
  • Add -type f when you mean files. It stops directories and odd entries from sneaking into a result you plan to delete or change.

When you need the full detail, the documentation is one command away:

$ man find        # the full manual page
$ find --help     # a quick summary of every test and action
$ info find       # the long GNU manual with examples
Back to top

9. Common Mistakes

9.1 Four Common Myths

MythReality
"-name takes a regular expression." It takes a shell glob (*, ?). For real regex use -regex.
"-mtime 7 means the last 7 days." Plain 7 means exactly 7 days ago. "Within 7 days" is -mtime -7.
"find searches inside files." It matches names and metadata only. Content search is grep's job.
"The order of tests does not matter." Order decides speed, and with -o and actions it can change the result.

9.2 Other Traps to Avoid

  • Unquoted wildcards. If the current folder contains one matching file, the shell expands *.log to that name and find searches for the wrong thing. Quote it.
  • Forgetting the \; or + after -exec. Without the terminator, find reports a "missing argument" error. The semicolon must be escaped as \;.
  • Running -delete too soon. There is no undo. Always list the matches first, and remember -delete quietly implies -depth, which changes traversal order.
  • Mixing -o with actions and no parentheses. Precedence surprises are common; group alternatives in \( ... \) and add an explicit -print when you also use another action.
  • Searching from / without limits. Starting at the root can crawl into /proc, /sys, and network mounts. Add -xdev to stay on one filesystem, or start somewhere narrower.
  • Assuming BSD and GNU match. Options like -regextype, -printf, and -delete are GNU extras. Keep them out of scripts meant to run on macOS.
Back to top

10. Summary

The find command is how you locate files on a Linux system by any property they have, and then act on them in one step.

  • find walks a directory tree and tests each entry against a description: name, type, size, age, owner, or permissions.
  • Its name is the plain English verb, but it does more than locate: it can run any command on each match.
  • The mental model is an expression that reads find WHERE WHAT-TO-MATCH WHAT-TO-DO, evaluated file by file.
  • Everyday tests: -name/-iname (by name), -type f or -type d (by kind), -size (by bytes), -mtime/-mmin (by age).
  • Tests join with an implicit "and"; use -o, !, and \( \) to build richer conditions.
  • -exec {} + runs a command on the matches fast, and -print0 | xargs -0 pipes them safely.
  • Order your tests cheapest-first for speed, and always preview with -print before using -delete.
  • When in doubt, type man find or find --help.

This is the quick reference worth keeping:

find . -name "*.log"            files named *.log, from here down
find . -iname "readme*"         same, ignoring upper/lower case
find /var -type f -size +100M   regular files larger than 100 MB
find /etc -mtime -1             changed in the last day
find . -mmin -30                changed in the last 30 minutes
find . -type d -empty           empty directories
find . -maxdepth 1 -type f      files in this folder only, no descending
find . \( -name "*.jpg" -o -name "*.png" \)   match either pattern
find . -name "*.tmp" -delete    delete matches (preview with -print first)
find . -name "*.tmp" -exec rm {} +            run one rm for many files
find . -name "*.log" -print0 | xargs -0 gzip  safe pipe for odd names
find . -path "*/.git" -prune -o -name "*.py" -print   skip a whole folder
find . -name "*.conf" -exec grep -l "debug" {} +      find + grep together

Once find feels natural, a Linux server stops hiding things from you. You can track down the file that changed last night, clear out the junk filling a disk, and reach every matching file in a huge tree with a single line. If your servers are holding files that someone needs to locate, clean up, or act on at scale, that is exactly the kind of work I help with.

Back to top
Linux command: find
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.