grep -rnF -e 'needle' ./srcThis searches files below ./src, prints matching lines with line numbers, and treats needle as literal text. Drop -F when the pattern should be a regular expression. I pass the search directory explicitly so the command is easy to review and behaves predictably across grep implementations.
The same recursive search works with current GNU grep on Linux and Apple's grep on macOS. The differences that matter are symlink handling and NUL-separated filename output, covered below.
The one-liner
Set the search path and pattern, then choose the command for your operating system.
grep -rn -e ':pattern' ':search_path'These examples interpret the pattern as a regular expression. For literal text, use grep -F or PowerShell's -SimpleMatch. Values containing a single quote need shell-appropriate escaping before you run the displayed command.
PowerShell recursion belongs to Get-ChildItem, not Select-String. -CaseSensitive makes its matching closer to grep's default. Reading a protected file can still fail; absence from the results is not proof that an unreadable file contains no match.
Recursive grep flags
| Flag | Result |
|---|---|
-r | Recurse into directories |
-n | Include the matching line number |
-H | Always include the filename, even with one input file |
-i | Ignore letter case |
-F | Match fixed strings rather than regular expressions |
-E | Use extended regular expressions |
-I | Skip files detected as binary |
-l | Print filenames containing a match |
-L | Print filenames with no matching line |
-e PATTERN | Supply a pattern explicitly, including one beginning with - |
grep -rnI -e 'TODO' .
grep -rnE -e 'error|warning' ./logs
grep -rniF -e 'connection refused' ./logsA search can return exit status 1 simply because nothing matched. Status 2 on GNU grep signals an error. Do not treat both outcomes as the same thing in a script.
Search only certain file types
Quote filename globs so the shell leaves them for grep:
grep -rn --include='*.ts' --include='*.tsx' -e 'useEffect' .
grep -rn --include='*.log' -e 'ERROR' ./logsCurrent macOS grep supports --include, --exclude and --exclude-dir. They are extensions rather than POSIX guarantees, so check the installed manual before relying on them in a script for an unknown host. There is no need to install GNU grep just to use these three flags on a current Mac.
Exclude directories and files
grep -rn --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist \
--exclude='*.min.js' --exclude='*.map' -e 'deprecated' .Use directory-name patterns for broad exclusions such as node_modules. GNU grep matches recursively discovered directories by their basename; a value such as frontend/node_modules is not a portable path-specific exclusion. find -prune gives explicit control over an exact path.
Repeated flags work in a POSIX shell. Bash and zsh also support the shorter brace-expansion form, but /bin/sh does not guarantee it:
grep -rn --exclude-dir={node_modules,.git,dist} -e 'deprecated' .Read exclude patterns from a file
GNU grep accepts a file of exclusion globs:
*.min.js
*.map
*.lockSave those lines as .grepignore, then run:
grep -rn --exclude-from=.grepignore --exclude-dir=node_modules -e 'deprecated' .This is a list of filename globs, not a .gitignore parser. Keep directory exclusions separate and do not assume negated Git ignore rules work. --include-from is not a corresponding GNU grep option; use repeated --include flags or select files with find. Check platform support for --exclude-from itself before sharing this command across systems.
Select files with find and prune
For an exact directory exclusion, prune the path before searching:
find . -path './frontend/node_modules' -prune -o \
-type f -name '*.ts' -exec grep -nH -e 'deprecated' {} +For every directory with either of two names:
find . -type d \( -name node_modules -o -name .git \) -prune -o \
-type f -exec grep -nH -e 'deprecated' {} +-exec ... {} + passes filenames as separate arguments, including spaces and newlines, and avoids an empty xargs invocation when no files are selected. A leading ./ on the paths also prevents a filename beginning with a hyphen from becoming a command option. See exclude directories with find for more selection examples.
List only filenames with grep -l
grep -rl -e 'deprecated' .
grep -rl --include='*.ts' --exclude-dir=node_modules -e 'deprecated' .Lowercase -l answers “which files contain a match?” and stops reading a file after its first match. It does not count occurrences. If you need counts, use the separate grep counting guide.
List files without a match with grep -L
grep -rL -e 'Copyright' ./srcCapital -L lists files with no matching line. It is different from -v, which selects nonmatching lines. Combining -v and -l lists files that have at least one nonmatching line, so it is not the inverse of -l.
On PowerShell, the equivalent file-level check is:
Get-ChildItem -LiteralPath './src' -Recurse -File |
Where-Object { -not (Select-String -LiteralPath $_.FullName -Pattern 'Copyright' -CaseSensitive -Quiet) } |
Select-Object -ExpandProperty FullNamePass matching filenames safely to another command
Newline-separated paths break when a filename itself contains a newline. For GNU and current Apple grep, the shared spelling for a NUL-separated list is --null:
grep -rl --null -F -e 'old' ./src | xargs -0 wc -lDo not substitute -Z on macOS. GNU grep uses -Z for NUL filename output; Apple's -Z enables decompression. The letters look portable while doing different jobs.
For an edit, preview the files first and keep a backup. This Bash loop changes the simple literal word old to new and creates a .bak file beside each changed file:
# Run in Bash, on a clean checkout; inspect the filename list first.
grep -rl --exclude='*.bak' -F -e 'old' ./src
grep -rl --null --exclude='*.bak' -F -e 'old' ./src |
while IFS= read -r -d '' file; do
sed -i.bak 's/old/new/g' "$file"
donesed -i.bak works with GNU and Apple sed. The replacement expression is still sed syntax: this is a fixed example, not a safe way to interpolate arbitrary search and replacement strings. Review the diff and the backups before keeping the changes. The loop performs no edits if no filename is emitted.
macOS BSD grep vs GNU grep
| Behavior | GNU grep | Current Apple grep |
|---|---|---|
| Recursive search | -r | -r or -R |
| Follow all symlinks while recursing | -R | -rS or -RS |
| Default recursive symlink behavior | -r follows command-line symlinks, skips those found during traversal | Skips symlinks; -O follows command-line links |
| Include/exclude file and directory globs | Supported | Supported |
| NUL after filenames | --null or -Z | --null |
-Z | NUL filename output | Decompress input |
Following links may leave the intended tree or encounter loops. I keep it disabled unless those linked paths are part of the task. A system with BusyBox grep or another BSD version can have different options again; use its own manual rather than transferring the Apple column blindly.
Common recursive-search mistakes
- An unquoted glob becomes shell arguments. Use
--include='*.log'; a regex such as.*is not a filename filter. For a literal string containing regex characters, add-F. - A missing path does not universally mean stdin. GNU recursive grep defaults to the current directory, and the current Apple binary does too. Plain nonrecursive grep without files reads stdin. Include
.or a named directory explicitly to remove the ambiguity. grepdoes not read.gitignore. Exclude dependency/build trees explicitly, or usergfor a search that follows ignore rules by default.- A filtered result is not a complete audit. Binary skips, permissions, exclusions and symlink rules all narrow coverage. Keep errors visible when completeness matters.
When to use another tool
Use git grep when the scope is tracked files in a repository. Use rg for day-to-day source searches with ignore-file support. Use find when modification time, permissions or path-specific selection matter. For structured JSON or XML, a parser such as jq or xmllint is less brittle than a text match.
FAQ
See also
Sources
Authoritative references this article was fact-checked against.
- GNU grep manualgnu.org
- Apple grep source manualraw.githubusercontent.com
- PowerShell Select-String referencelearn.microsoft.com





