TechEarl

How to Exclude a Directory in find (the -prune Pattern Explained)

find -path './node_modules' -prune -o -type f -print skips a directory subtree instead of walking into it. The pattern looks strange because -prune is an action, not a test, and the trailing -print is mandatory once you write an explicit action. The breakdown, the multi-directory form, the slower -not -path alternative, and when each one is the right call.

Ishan Karunaratne⏱️ 12 min readUpdated
Share thisCopied
Exclude a directory in find with -path './node_modules' -prune -o ... -print. Why the trailing -print is mandatory, the multi-directory form, the slower -not -path alternative, and BSD vs GNU notes.

find . -path './node_modules' -prune -o -type f -name '*.js' -print finds every JavaScript file under the current directory while skipping node_modules entirely. The expression looks odd the first time you see it: an -o, a -prune, and a trailing -print that feels redundant. None of it is decoration. Every piece is load-bearing, and dropping the wrong one quietly breaks the command.

The short version: -prune is an action that tells find "do not descend into this directory". The -o is a logical OR that splits the expression into the part you skip and the part you keep. The trailing -print is mandatory, because the moment you write any explicit action, find turns off the implicit print it would otherwise have given you for free.

Set your values

Try it with your own values

Set your OS, search path, and the directory to exclude. Every find example below updates with your values.

The canonical pattern

bash· Linux (GNU)
find :search_path -path './:exclude_dir' -prune -o -type f -print

That walks the tree, refuses to descend into the named directory, and prints every regular file everywhere else. Read left to right, the expression is two branches joined by -o (OR):

  1. -path './node_modules' -prune: if the current path matches, prune it.
  2. -type f -print: otherwise, if it is a regular file, print it.

Why the pattern looks weird

find evaluates an expression for every path it visits, left to right, and the operators short-circuit just like a shell && and ||. Three facts make the prune pattern make sense.

-prune is an action, not a test. Tests like -type f or -name '*.js' ask a yes/no question about the current path. Actions do something. -print prints, -delete deletes, and -prune tells find "do not recurse into this path". Critically, -prune always evaluates to true. So when -path './node_modules' matches and -prune runs, the whole left branch of the -o is true, find skips the subtree, and short-circuit OR means the right branch never runs for that path. Good, because you do not want to print node_modules itself.

The -o is mandatory. Without it, find would treat -prune and the rest as an implicit AND. find . -path './node_modules' -prune -type f -print reads as "path matches AND prune AND type f AND print", which only ever prints things inside the pruned directory, the exact opposite of the goal. The -o is what splits "the thing I throw away" from "the thing I keep".

The trailing -print is mandatory. This is the single most-asked thing about the pattern, so it gets its own section.

You must add the trailing -print

find . -type f prints its matches without you asking. That is the implicit print: when an expression contains no explicit action, find appends -print for you.

The moment you write an explicit action anywhere in the expression, that free print disappears. -prune is an action. So the second you add -prune, find stops printing automatically, and an expression like find . -path './node_modules' -prune -o -type f produces almost nothing useful. You have to put the -print back yourself, attached to the keep branch:

bash
# WRONG: -prune is an action, so the implicit print is gone.
# This prints nothing from the right branch.
find . -path './node_modules' -prune -o -type f

# RIGHT: explicit -print on the keep branch.
find . -path './node_modules' -prune -o -type f -print

Mnemonic: if your expression has a -prune, it also needs a -print (or -print0, or -exec). One explicit action begets another.

Exclude multiple directories

For more than one directory, group the -path tests with escaped parentheses and join them with -o, then prune the whole group:

bash· Linux (GNU)
find :search_path \( -path './:exclude_dir' -o -path './.git' -o -path './dist' \) -prune -o -type f -print

The \( ... \) parentheses are escaped because the shell would otherwise treat bare parentheses as a subshell. Inside the group, the -o chain says "path is node_modules OR .git OR dist". The single -prune after the group prunes whichever one matched. The -o -type f -print keep branch is unchanged.

To match a directory anywhere in the tree (not just at the top level), drop the ./ anchor and wrap the name in glob stars: -path '*/node_modules'. That matches node_modules at any depth, which is what you usually want in a project with nested packages.

The simpler -not -path alternative

There is a second way to exclude a directory that reads far more naturally:

bash· Linux (GNU)
find :search_path -type f -not -path '*/:exclude_dir/*'

-not -path '*/node_modules/*' reads plainly: "every file whose path does not contain a node_modules segment". No -prune, no -o, no trailing -print rule to remember. The implicit print is still in effect because every part here is a test, not an action.

The catch is what find actually does under the hood. -not -path is a filter, not a skip. find still descends into node_modules, walks every single file inside it, evaluates the -path test against each one, and discards the matches. The directory never gets pruned. You get the right output, but find did all the work of walking the subtree first.

-prune is different. When -prune fires on a directory, find never opens it. The entire subtree is skipped at the filesystem level, with zero stat calls and zero directory reads inside it.

Performance: -prune vs -not -path

On a small tree the difference is invisible. On a real project it is not. A typical node_modules holds tens of thousands of files across thousands of nested directories. With -not -path, find opendirs and stats every one of them before throwing the results away. With -prune, find skips the directory the moment it sees the name.

Tree-prune-not -path
Small project, no big subtreeSameSame
Project with a 40k-file node_modulesSkips the subtree, fastWalks all 40k files, then filters
Repeated in a watch loop or CI stepCheap every runPays the full walk every run
Network or FUSE filesystemAvoids slow stat callsTriggers a stat per file

If the command runs once interactively, reach for -not -path because it is easier to read and the speed gap does not matter. If it runs in a hot path (a file watcher, a pre-commit hook, a CI step, anything on a network mount), use -prune so find never pays to walk the directory you are throwing away.

macOS BSD vs GNU find

Both -prune and -path are POSIX, so the pattern is portable. macOS ships BSD find, Linux ships GNU find, and the exclude pattern works identically on both.

FeatureGNU find (Linux)BSD find (macOS default)
-prune actionSupportedSupported
-path testSupportedSupported
-not operatorSupportedSupported (! also works)
\( ... \) groupingSupportedSupported
-o / -a operatorsSupportedSupported
Implicit-print suppression by actionsYesYes
-ipath (case-insensitive path)SupportedSupported

The implicit-print rule is the same on both: any explicit action, including -prune, turns off the automatic -print. The trailing -print is just as mandatory on macOS as it is on Linux. If you have aliased find to GNU gfind via brew install findutils, the pattern is unchanged.

Common find -prune mistakes

1. Forgetting the trailing -print. The number one bug. find . -path './node_modules' -prune -o -type f runs clean and prints almost nothing, because -prune killed the implicit print. Always end the keep branch with -print, -print0, or an -exec.

2. Wrong -path glob. -path matches the whole path string find is currently looking at, starting from the search root. If you searched find ., paths look like ./node_modules/foo, so the test must be -path './node_modules' with the ./ prefix. If you searched find /var/www, paths start with /var/www, so use -path '/var/www/node_modules' or the depth-agnostic -path '*/node_modules'. A -path 'node_modules' with no slashes matches nothing.

3. Putting -prune after the keep tests. Order matters because find short-circuits left to right. find . -type f -o -path './node_modules' -prune evaluates -type f first; for a directory that fails, find moves to the -prune branch, but it has already descended. The prune test and -prune action must come before the -o.

4. Bare parentheses instead of escaped ones. find . ( -path ... ) makes the shell try to open a subshell and errors out before find even runs. Escape them: \( ... \), or quote them: '(' ... ')'.

5. Using -prune to exclude files. -prune only does something meaningful on directories, because "do not descend" only matters for things you can descend into. To exclude files by name, use a plain negated test: -not -name '*.min.js'.

6. Pruning the search root itself. find node_modules -path 'node_modules' -prune ... prunes the very directory you asked it to search, so it returns nothing. Prune subdirectories of the search path, not the path you passed in.

When NOT to use -prune

-prune earns its complexity on big trees and hot paths. Skip it when:

  • The tree is small. If there is no large subtree to skip, -not -path '*/dir/*' is easier to read and just as fast. Save the cognitive load.
  • You want one-off interactive results. Typing the prune pattern correctly under time pressure is error-prone. For a quick find . -type f -not -path '*/.git/*' at the terminal, the simple form wins.
  • You are excluding by filename, not by directory. -prune is about not descending into directories. For "skip every .min.js", a negated -name test is the right tool.
  • A higher-level tool already respects ignore files. ripgrep, fd, and git ls-files honor .gitignore automatically, so node_modules and friends are excluded with zero flags. If the job is "search project files", fd or rg is often simpler than any find expression.

See also

FAQ

TagsfindpruneCLILinuxmacOSBSDnode_modulesShell Scripting

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Skip node_modules, .git, and build output from grep with --exclude-dir, exclude filename globs with --exclude, and search only matching files with --include. The one-liner, brace expansion, --exclude-from, and the BSD grep fallback.

How to Exclude Files and Directories from grep

grep does not read .gitignore, so skipping node_modules, .git, and build output is on you. The flags that do it: --exclude for filename globs, --exclude-dir for whole directories, --include for the inverse, --exclude-from to read the list from a file, plus the find -prune fallback for older macOS grep.

Three patterns for counting ACF Repeater rows: count() on the raw field, get_field_count, and a fast meta-only count without loading the rows.

How to Count Rows in an ACF Repeater Field

Counting ACF Repeater rows is three short patterns: count() on the raw field, get_field_count() inside a loop, and a faster meta-only count that skips loading the rows. Each has its right use case.

Convert a PSD to PNG from the command line with ImageMagick using the [0] composite-layer trick, extract individual layers, batch a folder, and the Maximize Compatibility caveat.

How to Convert a PSD to PNG From the Command Line

Convert a PSD to PNG from the command line with ImageMagick. The one trick that matters: design.psd[0] selects the flattened composite, so you get one PNG instead of a folder full of separate layers.