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 KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
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
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

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.

Use grep -v 'pattern' file to print every line that does not match. Exclude multiple patterns with -e or -vE, strip comments and blank lines, count with -vc, and avoid the OR-becomes-AND double-negative trap.

How to Exclude Matches with grep -v (Invert Match)

grep -v 'pattern' file prints every line that does NOT match. The flag reference, how to exclude multiple patterns, the strip-comments-and-blank-lines pipeline, the double-negative trap where -v of an OR becomes an AND of negations, and the macOS BSD vs GNU differences.

Archive every file matching a find pattern with tar. The safe find -print0 | tar --null --files-from=- one-liner, the macOS BSD tar -T difference, archiving by modification time, and gzip vs bzip2 vs xz vs zstd.

How to Archive Files Matching a find Pattern with tar

find locates the files, tar archives them. The safe pairing is find -print0 piped into tar reading a NUL-delimited list from stdin: no breakage on spaces or newlines. The flag breakdown, the macOS BSD tar vs GNU tar difference, the -exec append alternative, archiving by modification time, and the compression choices.