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
Set your OS, search path, and the directory to exclude. Every find example below updates with your values.
The canonical pattern
find :search_path -path './:exclude_dir' -prune -o -type f -printThat 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):
-path './node_modules' -prune: if the current path matches, prune it.-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:
# 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 -printMnemonic: 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:
find :search_path \( -path './:exclude_dir' -o -path './.git' -o -path './dist' \) -prune -o -type f -printThe \( ... \) 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:
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 subtree | Same | Same |
Project with a 40k-file node_modules | Skips the subtree, fast | Walks all 40k files, then filters |
| Repeated in a watch loop or CI step | Cheap every run | Pays the full walk every run |
| Network or FUSE filesystem | Avoids slow stat calls | Triggers 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.
| Feature | GNU find (Linux) | BSD find (macOS default) |
|---|---|---|
-prune action | Supported | Supported |
-path test | Supported | Supported |
-not operator | Supported | Supported (! also works) |
\( ... \) grouping | Supported | Supported |
-o / -a operators | Supported | Supported |
| Implicit-print suppression by actions | Yes | Yes |
-ipath (case-insensitive path) | Supported | Supported |
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.
-pruneis about not descending into directories. For "skip every.min.js", a negated-nametest is the right tool. - A higher-level tool already respects ignore files.
ripgrep,fd, andgit ls-fileshonor.gitignoreautomatically, sonode_modulesand friends are excluded with zero flags. If the job is "search project files",fdorrgis often simpler than any find expression.
See also
- find Command Cheat Sheet: the full find reference covering name, type, size, permissions, and
-execpatterns - Find files containing text (find + grep): pair the prune pattern with grep to search only the files you kept
- Find files by extension: the
-nameand-inamereference, the natural partner to a-pruneexclude - External: GNU findutils manual, FreeBSD find(1) man page.





