TechEarl

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.

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

grep -r --exclude-dir=node_modules 'pattern' . searches a whole tree but skips the node_modules directory. That single flag is the most-asked grep question there is, because grep does not read .gitignore. Unlike ripgrep, which auto-skips ignored paths, grep searches everything under the path you give it: build output, vendored dependencies, .git internals, minified bundles. Excluding them is manual, and the flags that do it are --exclude, --exclude-dir, and --include.

This page covers all three, the one-liner I actually paste, how to stack multiple excludes, reading the glob list from a file, and the find -prune fallback for the older BSD grep that shipped on macOS before version 12.

Set your values

Try it with your own values

Set your OS, search path, and the pattern. Every grep example below updates with your values.

The one-liner

The command that covers the common case: search recursively, skip the three directories nobody wants in their results.

bash· Linux (GNU)
grep -rn --exclude-dir={.git,node_modules,dist} ':pattern' :search_path

The {.git,node_modules,dist} part is brace expansion. The shell expands it into three separate --exclude-dir arguments before grep ever runs, so the command grep receives is really --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=dist. That distinction matters and I come back to it under common mistakes.

-r makes the search recursive, -n prefixes each match with its line number. Add -I (capital i) to also skip binary files.

Exclude files by name with --exclude

--exclude=GLOB skips any file whose name matches the glob. The classic use is dropping minified assets and lock files from a code search.

bash· Linux (GNU)
grep -rn --exclude='*.min.js' ':pattern' :search_path

The glob matches against the file name only, not the full path. *.min.js catches app.min.js and vendor.min.js anywhere in the tree. Quote the glob so the shell does not expand it against the current directory first.

To stack multiple file excludes, repeat the flag or use brace expansion:

bash· Linux (GNU)
grep -rn --exclude={'*.min.js','*.map','*.lock'} ':pattern' :search_path

Both forms produce the same result. Brace expansion is shorter; repeated --exclude= flags are clearer in a script someone else will read.

Exclude whole directories with --exclude-dir

--exclude-dir=NAME is the one most people are actually looking for. It prunes an entire directory subtree, so grep never descends into it at all. That is faster than --exclude on files, because grep skips the directory wholesale instead of testing every file inside it.

bash· Linux (GNU)
grep -rn --exclude-dir=node_modules ':pattern' :search_path

Multiple directory excludes work the same way as file excludes: repeat the flag, or brace-expand.

bash· Linux (GNU)
grep -rn --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist ':pattern' :search_path

One important rule: --exclude-dir takes a directory name, not a path. --exclude-dir=node_modules skips every node_modules directory anywhere in the tree, which is usually what you want in a monorepo. --exclude-dir=src/node_modules does not work the way you expect because it is matched as a glob against the directory name, and a name never contains a slash. If you need to exclude one specific path and not others, the find -prune approach below gives you that control.

Search only matching files with --include

--include=GLOB is the inverse of --exclude. Instead of listing what to skip, you list what to keep, and grep searches only files whose name matches. This is the cleaner choice when you want one file type and nothing else.

bash· Linux (GNU)
grep -rn --include='*.py' ':pattern' :search_path

That searches only Python files. No node_modules problem to solve, because .js files under node_modules never match *.py in the first place. When your search is type-specific, --include is simpler than chaining excludes.

--include also stacks. To search a couple of file types:

bash· Linux (GNU)
grep -rn --include='*.ts' --include='*.tsx' ':pattern' :search_path

You can combine --include and --exclude-dir in the same command. Search only TypeScript, but still skip node_modules so vendored .d.ts files do not pollute the results.

Read the glob list from a file

When the same set of excludes shows up across many commands, hard-coding it gets repetitive. --exclude-from=FILE reads the exclude globs from a file, one per line. There is a matching --include-from=FILE for the inverse.

First, the glob file. One pattern per line, no quotes (the shell is not involved):

text
*.min.js
*.map
*.lock
package-lock.json

Then point grep at it:

bash· Linux (GNU)
grep -rn --exclude-from=.grepignore ':pattern' :search_path

One caveat worth knowing: --exclude-from lists file globs only. There is no --exclude-dir-from. If you want a single file to hold directory excludes too, you either keep them in the command line or wrap the whole thing in a shell function. This is the closest grep gets to a .gitignore, and it is not very close.

grep does not read .gitignore

This is the root of the confusion. grep has no concept of an ignore file. It searches exactly what you point it at, every file under the path, including everything git has been told to ignore. The .gitignore-aware behavior people expect belongs to ripgrep (rg), which skips ignored paths by default and is the reason rg "just works" inside a repo without any exclude flags.

If you want grep-the-command to respect .gitignore, there are two practical routes:

  • Use git grep instead. It only searches files that git tracks, so ignored and untracked files are excluded automatically. The tradeoff: it misses files you have not staged yet.
  • Switch to ripgrep. rg 'pattern' skips .gitignore entries, .ignore entries, and hidden files by default. For local development inside a repo this is almost always the better tool.

For everything else (production servers where you cannot install extras, searching paths that are not git repos at all), the manual --exclude-dir flags are what you have.

macOS: BSD grep vs GNU grep

--include, --exclude, and --exclude-dir are GNU extensions. They are not in the POSIX spec. The older BSD grep that macOS shipped for years did not have them at all, and a script using --exclude-dir would fail outright on a 2019 Mac.

FlagGNU grepBSD grep (older macOS)BSD grep (macOS 12+)
--include=GLOBSupportedNot supportedSupported
--exclude=GLOBSupportedNot supportedSupported
--exclude-dir=NAMESupportedNot supportedSupported
--exclude-from=FILESupportedNot supportedSupported
--include-from=FILESupportedNot supportedSupported

macOS 12 (Monterey) and later added these flags to the system grep, so on a current Mac the GNU-style commands on this page work as written. If you have to support older macOS, or you just want certainty, you have two choices: install GNU grep with brew install grep and call it as ggrep, or fall back to find with -prune.

The fallback prunes the directory in find, then pipes the surviving files into xargs grep:

bash· Linux (GNU)
find :search_path -type d -name node_modules -prune -o -type f -print0 | xargs -0 grep -n ':pattern'

-print0 and xargs -0 use NUL separators so filenames with spaces survive the pipe. The find -prune article covers the -prune operator in full, including how to prune several directories at once.

Common mistakes

1. Passing a path to --exclude-dir instead of a name. --exclude-dir=app/node_modules does not exclude that one path. The value is matched as a glob against directory names, and names never contain slashes, so it matches nothing. Use --exclude-dir=node_modules to skip the directory everywhere, or find -prune when you genuinely need one specific path excluded.

2. Thinking brace expansion is a grep feature. {.git,node_modules,dist} is expanded by the shell, not by grep. grep only ever sees the expanded result. This means brace expansion works in Bash and Zsh but not in sh/dash, and not at all if the braces are quoted. If --exclude-dir={.git,dist} mysteriously fails, check what shell the script runs under, and check the braces are unquoted.

3. Forgetting grep ignores .gitignore. A search returns ten thousand hits from node_modules and the instinct is "grep is broken". grep is doing exactly what it was told. It has no ignore file. Either add --exclude-dir, switch to git grep, or use ripgrep.

4. Quoting the glob wrong. --exclude=*.min.js unquoted lets the shell expand *.min.js against the current directory before grep runs. If a matching file exists in your cwd, grep receives that filename as the exclude value and silently excludes the wrong thing. Always quote: --exclude='*.min.js'.

5. Expecting --exclude to match a path. Like --exclude-dir, --exclude matches against the file name only. --exclude='build/*.js' does not work, because the file name has no build/ in it. Exclude the directory with --exclude-dir=build instead.

When NOT to use these flags

The manual exclude flags are the right tool when you are searching arbitrary paths outside a git repo, or on a server where you cannot install anything. Inside a repo, there is usually a better option:

  • Use ripgrep for local code search. rg respects .gitignore automatically, skips hidden files and binaries by default, and is dramatically faster on large trees. You almost never need an exclude flag with rg. See grep vs ripgrep vs ag for the full comparison.
  • Use git grep when you only care about tracked files. It searches the git index, so ignored and untracked files are excluded for free, with no flags. The limitation: brand-new files you have not added yet are invisible to it.
  • Reach for find -prune when you need path-specific exclusion. grep's --exclude-dir matches names globally. If you must exclude frontend/dist but keep backend/dist, find with -prune is the tool that can express that.

See also

FAQ

TagsgrepCLILinuxmacOSBSDnode_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

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.

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.

Use find ... -print0 | xargs -0 grep -l 'PATTERN' to find every file containing a string. When grep -r is enough, when to add find as a pre-filter for performance, multi-pattern matching with -E, and the safe NUL-delimited pipeline.

How to Find Files Containing Specific Text (find + grep)

find ... -print0 | xargs -0 grep -l 'PATTERN' finds every file containing a piece of text. The combo handles weird filenames, scales to huge trees, and replaces three other common but broken pipelines. When to use grep -r alone, when to add find as a pre-filter, and the BSD vs GNU pitfalls.

Use find -type d -empty to list empty directories and find -type f -empty for empty files. The -depth trap for deleting nested empty trees, the hidden-file gotcha, the safe two-pass cleanup, and BSD vs GNU find notes.

How to Find (and Delete) Empty Directories and Files

find . -type d -empty lists every empty directory; find . -type f -empty lists every empty file. The catch is what 'empty' means (a hidden file makes a directory not empty) and the -depth trap that lets find -delete collapse whole nested empty trees in one pass. The flag reference, the safe two-pass cleanup, the BSD vs GNU notes, and the mistakes that bite.