grep -r 'pattern' . searches every file under the directory you point it at. The -r flag turns the file argument into a starting directory and grep walks the whole tree from there, printing path:line for every match. The . at the end is the search root, and it is the single most-forgotten part of the command.
The thing to know up front: -r does not follow symlinked directories, -R does. Pick -r unless you specifically need symlink traversal, because -R can walk into places you did not expect (and, in the worst case, loop). Everything else on this page is filtering: scoping the crawl to certain file types, skipping node_modules and .git, and trimming the output down to filenames or line numbers.
Set your values
Set your OS, search path, and pattern. Every grep example below updates with your values.
The one-liner
grep -r ':pattern' :search_pathThat walks every file under :search_path and prints each matching line prefixed with its filename. The . (or whatever path you set) is mandatory. grep -r ':pattern' with no path makes grep read from standard input and hang as if it is waiting for you to type, which is the classic "why is my terminal frozen" moment.
-r vs -R: the symlink difference
Both flags make grep recursive. The difference is what they do with symbolic links to directories.
| Flag | Recursive | Follows symlinked directories |
|---|---|---|
-r | Yes | No |
-R | Yes | Yes |
--include / --exclude-dir | n/a | Works with both (GNU) |
-r traverses real directories only. If your tree contains a symlink pointing at another directory, -r lists the link but does not descend into it. -R follows the link and searches the target as if it were part of the tree.
Default to -r. Use -R only when you know the symlinks are intentional and you want their contents included. The risk with -R is symlink loops: a link that points back up the tree makes grep recurse forever (GNU grep detects most loops and warns, but BSD grep is less forgiving).
Recursive search filtered to certain files
A bare grep -r searches everything: source files, lock files, minified bundles, binary assets. Most of the time you want one file type. GNU grep has --include for exactly this.
grep -r --include='*.log' ':pattern' :search_path--include='*.log' restricts the crawl to files matching the glob. You can pass it more than once (--include='*.js' --include='*.ts') to cover several extensions. BSD grep on macOS has no --include, so the portable fallback is find ... -print0 | xargs -0 grep, which the find files containing text article covers in depth.
Recursive search listing only filenames
When you only care which files contain the pattern, not the matching lines, add -l.
grep -rl ':pattern' :search_path-rl prints one filename per matching file and stops reading that file after the first hit, so it is faster than a full search. It is the right form when you want to pipe the file list into another command. The grep list filenames article goes deeper on -l, -L (files without a match), and feeding the output into xargs.
Recursive search with line numbers
For code search you almost always want line numbers so you can jump straight to the hit.
grep -rn ':pattern' :search_path-rn prints path:linenumber:matchedline. Most editors and terminals make that path:line prefix clickable, which turns grep into a navigation tool. Add -I (capital i) to skip binary files, which both GNU and BSD grep support, and the output stays clean even when the tree contains images or compiled artifacts.
grep -r vs find -exec grep
grep -r and find ... -exec grep both search a tree. They are good at different things.
Reach for grep -r when the filter is simple: a glob on the filename (--include) or a directory to skip (--exclude-dir). It is one command, it is fast, and the output formatting is built in.
Reach for find ... -exec grep (or find ... | xargs grep) when the file selection needs logic grep cannot express: files modified in the last day, files larger than a size, files with a specific permission bit, files matching a regex on the path rather than a simple glob. find does the selection, grep does the content match.
find :search_path -name '*.log' -mtime -1 -print0 | xargs -0 grep -n ':pattern'That example ("recent .log files containing the pattern") is something grep -r alone cannot do, because grep has no concept of modification time. The -print0 / -0 pairing is important: it uses NUL as the separator so filenames with spaces survive the pipe. The find files containing text article is the full treatment of this pattern.
Excluding .git and node_modules
A recursive search inside a project directory will, by default, crawl .git and node_modules. That is thousands of files you never want in the results, and it makes the search slow and the output unreadable. GNU grep has --exclude-dir.
grep -rn --exclude-dir=node_modules --exclude-dir=.git ':pattern' :search_pathYou can pass --exclude-dir as many times as you need. There is also --exclude='*.min.js' for skipping files by glob, and --exclude-from=FILE for reading a list of patterns from a file. BSD grep on macOS has none of these, so the macOS variant uses find with -prune to skip the directories before they are searched. The grep exclude files and directories article covers every exclude flag and the -prune equivalents.
If you do this on every search, the cleaner fix is ripgrep, which honours .gitignore automatically and never needs an --exclude-dir flag.
macOS BSD grep vs GNU grep
macOS ships BSD grep, not GNU grep, and the recursive-search story diverges in a few places that matter.
| Behavior | GNU grep (Linux) | BSD grep (macOS default) |
|---|---|---|
-r recursive | Supported | Supported |
-r follows symlinks | No (use -R) | No (use -R), but older macOS followed by default |
-R recursive + symlinks | Supported | Supported |
--include=GLOB | Supported | Not supported |
--exclude=GLOB | Supported | Not supported |
--exclude-dir=DIR | Supported | Not supported |
-I skip binary files | Supported | Supported |
-l / -n recursive | Supported | Supported |
The headline gap is the filtering flags. --include, --exclude, and --exclude-dir are GNU-only, and they are exactly the flags you reach for during recursive search. On macOS you either fall back to find ... | xargs grep, or install GNU grep with brew install grep and call it as ggrep (alias it to grep in your shell rc and the friction disappears). The symlink default also drifted across macOS releases, so if a script depends on it, be explicit: -r for "do not follow", -R for "follow".
Common grep -r mistakes
1. Forgetting the path argument. grep -r 'pattern' with no directory makes grep read from standard input. The terminal looks frozen; it is actually waiting for you to type or pipe data in. Always end the command with a path, even if it is just . for the current directory.
2. Letting -r crawl into huge directories. Running grep -r from a project root with no --exclude-dir searches node_modules, .git, dist, vendor, and every other heavy directory. The search is slow and the signal drowns in noise. Add --exclude-dir for the obvious offenders, or use git grep inside a repo.
3. Using -R when -r was meant. -R follows symlinked directories. If your tree has a symlink that points back up toward an ancestor, -R can recurse into a loop. GNU grep detects most loops and prints a warning; BSD grep handles it less gracefully. Use -r unless you have a concrete reason to follow links.
4. Unquoted patterns. grep -r *.log . lets the shell expand *.log into a file list before grep ever runs. Quote the pattern: grep -r '*.log' .. The same applies to any pattern containing *, ?, [, or $.
5. Expecting --include to work on macOS. It silently does nothing useful on BSD grep, or errors, depending on the version. If a recursive search "ignores" your --include, check whether you are on the system grep. Switch to ggrep or the find | xargs form.
6. Mismatched separators in the find pipeline. find ... | xargs grep breaks on filenames with spaces. Use find ... -print0 | xargs -0 grep so NUL separates the entries instead of whitespace.
When NOT to use grep -r
grep -r is the universal tool, available on every Unix box with no install. But it is not always the best one.
- Use
ripgrep(rg) for speed in a repo. It is dramatically faster on large trees, recursive by default, and respects.gitignoresonode_modulesand build output are skipped without any flags. For day-to-day code search on a workstation,rg 'pattern'beatsgrep -rn --exclude-dir=... 'pattern' .on both speed and ergonomics. - Use
git grepinside a git repository. It searches only tracked files, so it never touches.git, ignored build artifacts, or untracked junk, and it is fast because it works off git's index.git grep -n 'pattern'is the right call when "the codebase" means "files git knows about". - Use
find -exec grepwhen file selection needs logic. Modification time, size, permission bits, path regex: anything beyond a filename glob belongs tofind. See find files containing text. - Use a structured-data tool for structured data.
grep -ris line-oriented and content-blind. For JSON,jq; for XML,xmllint --xpath; for CSV,csvgrep. Greping structured formats works until a value spans lines or a delimiter appears inside a field.
For a one-off search on a production server where you cannot install anything, grep -r is exactly right. For repeated searches in a checkout you control, rg or git grep will serve you better.
See also
- grep cheat sheet: the full grep reference covering regex flavors, context lines, counting, and the BSD vs GNU divergences
- grep to list only filenames:
-l,-L, and feeding the matched-file list intoxargs - Exclude files and directories from grep: every
--excludeflag,--exclude-dir,--exclude-from, and the-pruneequivalents for BSD grep - Find files containing text: when the file selection needs
find's logic before grep runs - External: GNU grep manual, FreeBSD grep(1) man page.





