Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Linux find Command Recursive Search: Getting the Right Files Without the Noise

Sean

Platform Writer

Aug 17, 2026
9 min read

find is already recursive. There is no -r flag to add, and looking for one is the most common reason this search gets typed. find . -name '*.log' walks the entire tree under the current directory and always has.

Linux find Command Recursive Search: Getting the Right Files Without the Noise

What people usually mean when they reach for a recursive flag is something narrower: search below this point but not forever, skip the directories I do not care about, and stop drowning me in permission-denied lines. Those are all solvable, and the answers are worth knowing because find is the tool you fall back to when the fancier ones are not installed on the box you have been dropped into.

Table of contents

The basic shape, and the quoting rule that trips everyone

The command reads as: where to look, what to match, what to do about it.

find . -name '*.log'
find /var/log -name 'syslog*'
find . -type f -name '*.conf'

Quote the pattern. Always. Without quotes, the shell expands *.log against the current directory before find ever sees it, so you end up passing whatever happened to match in the working directory as the -name argument. On a directory with exactly one .log file it silently appears to work, which is worse than failing, because you learn the wrong lesson and get bitten later on a directory with three.

-name is case-sensitive. -iname is not. On a codebase that has accumulated README, readme, and ReadMe over the years, -iname is what you want.

The path argument can be a list. find src tests docs -name '*.py' searches three trees in one pass and is faster than three invocations, because the traversal cost dominates.

Controlling depth

Unlimited recursion is the default and is frequently not what you want, especially in a directory with a node_modules in it.

# current directory only, no recursion at all
find . -maxdepth 1 -name '*.txt'

# skip the top level, search everything below it
find . -mindepth 2 -name '*.txt'

# two levels down at most
find . -maxdepth 2 -type d -name 'dist'

Put -maxdepth before the other tests. GNU find warns if you do not, and the reason is real: the expression is evaluated left to right, so a depth limit placed after a test has already let the traversal happen. It still works, but the warning is telling you the ordering is not doing what it looks like.

-maxdepth 1 is the honest way to say do not recurse, and it is worth knowing because it is what you actually wanted when you went looking for the recursive flag.

Filtering by type, size, and time

-type is the filter that removes the most noise for the least effort. f for regular files, d for directories, l for symlinks.

find . -type d -name 'node_modules'
find . -type l -name '*.so'

# files over 100 megabytes
find /var -type f -size +100M

# changed in the last day
find /etc -type f -mtime -1

# not touched in ninety days
find /backups -type f -mtime +90

The sign on the time and size arguments is the part people get backwards. -mtime -1 means less than one day ago, so recent. -mtime +90 means more than ninety days ago, so old. No sign means exactly, which is almost never what you want and is a good source of empty result sets.

-mmin works the same way in minutes and is far more useful during an incident. find /var/log -mmin -15 answers which log files were written in the last quarter hour, which is often the fastest route to the component that broke.

Size suffixes matter too. Bare -size +100 means 100 blocks of 512 bytes, not 100 bytes and not 100 megabytes. Write +100M and move on.

Silencing permission-denied noise

Running find / as a normal user buries the three lines you wanted under a few hundred Permission denied errors. Those go to stderr, so they are easy to separate.

# throw the errors away
find / -name 'nginx.conf' 2>/dev/null

# GNU find: skip unreadable directories cleanly
find / -name 'nginx.conf' -readable 2>/dev/null

# keep errors visible but out of a pipeline
find / -name '*.pem' 2>err.log | sort

2>/dev/null is the reflex, and it is fine most of the time. It is worth pausing when the search is meant to be exhaustive, though — you have just discarded the evidence that the search was incomplete. During a security review, redirecting to a file and reading it afterwards is the better habit.

The other half of the noise problem is directories you never want to descend into. -prune handles that, and its syntax is genuinely awkward, which is why nobody remembers it:

find . -path './node_modules' -prune -o -name '*.js' -print
find . \( -name node_modules -o -name .git \) -prune -o -type f -print

The -o is a logical OR and the trailing -print is required. Without it, the default print action applies to the pruned branch too and the exclusion appears not to work. That single missing -print is responsible for a lot of confusion about whether -prune is broken. It is not.

Acting on the results

Finding files is half the job. -exec and -delete do something with them, and the difference between the two forms of -exec is worth understanding because one is much slower.

# one process per file -- correct, slow
find . -name '*.tmp' -exec rm {} \;

# batches arguments -- correct, much faster
find . -name '*.tmp' -exec rm {} +

# safest for filenames with spaces or newlines
find . -name '*.tmp' -print0 | xargs -0 rm

The \; form spawns a new process for every single match. On a few files nobody notices. On forty thousand it is the difference between a second and several minutes. The + form passes as many paths as fit on one command line, the same way xargs does, and should be the default.

Before running anything destructive, run it with -print or ls first and read the list. find . -name '*.conf' -delete with a typo in the pattern is a bad afternoon, and find will not ask you to confirm. There is an -ok variant of -exec that prompts per file, which is tedious enough that people disable it — dry-running the match is the habit that actually sticks.

-print0 paired with xargs -0 is the correct answer whenever filenames might contain spaces, which on any machine that has ever seen a file from a desktop is all of them.

When find is the wrong tool

find matches on metadata: names, types, timestamps, permissions, ownership. It does not look inside files. If the question is which file contains this string, that is grep -r, and grep -r has been recursive for a long time now.

grep -rn 'DATABASE_URL' .
grep -rn --include='*.py' 'DATABASE_URL' .

# combine when you need both
find . -name '*.env' -exec grep -l 'SECRET' {} +

On a developer machine, fd and ripgrep are faster and respect .gitignore by default, which removes the node_modules problem entirely. They are worth installing locally. They are also worth not depending on, because the container you are debugging at two in the morning will have find and nothing else.

And for the specific case of locating an installed binary or a package file on a system with a maintained index, locate answers instantly from a database instead of walking the filesystem. It is only as fresh as the last updatedb run, which makes it excellent for system files and useless for files you created five minutes ago.

How this fits the rest of the stack

Reaching for find on a production box usually means going in to look at something by hand — which log grew, which config got edited, which upload directory quietly filled the disk. That is a reasonable thing to do occasionally and a bad thing to depend on weekly. Deploy logs and runtime logs that are already collected, searchable, and attached to the deploy that produced them answer most of those questions without an SSH session at all; Services on RunxBuild covers how logs, metrics, and rollback sit together per service. If disk growth is what sent you here, persistent storage and bandwidth are separate line items rather than a surprise, and the RunxBuild hosting calculator shows them alongside the service and database so the total is visible before you commit to it.

Useful related references:

FAQ

Is the Linux find command recursive by default?

Yes. find walks the entire directory tree below the path you give it, with no flag required. There is no -r or -R option because recursion is the default behaviour. Use -maxdepth 1 if you want to search a single directory without descending.

How do I stop find from printing permission denied errors?

Those messages go to stderr, so redirect it: find / -name '*.conf' 2>/dev/null. On GNU find you can also add -readable to skip directories you cannot read. Be aware that discarding the errors also discards the evidence that your search was incomplete.

How do I exclude node_modules from a find search?

Use -prune with an OR and an explicit print: find . -name node_modules -prune -o -type f -print. The trailing -print is required — without it the default print action still applies to the pruned branch and the exclusion appears to do nothing.

What is the difference between -exec with a semicolon and with a plus?

-exec cmd {} \; runs the command once per matched file. -exec cmd {} + batches as many files as fit onto one command line, like xargs does. The plus form is dramatically faster on large result sets and should be your default.

Should I use find or grep to search inside files?

find matches on metadata — name, type, size, timestamp, permissions. It never reads file contents. To search inside files use grep -rn 'pattern' ., which is recursive. Combine them when you need both: find . -name '*.env' -exec grep -l SECRET {} +.

#linux find command recursive#find command#linux search files#bash#command line