grep searches text for lines matching a pattern. The flag set is small but the gotchas are not: macOS ships BSD grep without -P (PCRE), --include, or --exclude, and --color=auto is a GNU-ism. This page is the reference I keep open while writing shell pipelines, with every command shown in the variant that actually works on the platform you pick at the top.
What is grep used for?
grep is the de facto tool for searching text on the command line. You pass it a pattern (literal string, basic regex, extended regex, or PCRE depending on the flag) and one or more files, and it prints every line that matches. The most common uses: filtering log files for an error string, finding all callers of a function in a codebase, narrowing a long command's output to the parts you care about, and counting matches with -c. Recursive search with -r makes it a quick code-grep tool, though dedicated tools like ripgrep are faster for large repositories. The catch on macOS is that the system grep is BSD, not GNU, so a handful of GNU-only flags (-P, --include, --exclude, the auto value on --color) do not work. The Windows equivalent for most patterns is PowerShell's Select-String cmdlet; the older findstr command works in cmd.exe but lacks proper regex support.
Jump to:
- Basic search
- Regex flavors: BRE, ERE, PCRE
- Common flags
- Recursive search
- Filtering by filename
- Context lines
- Counting, listing, inverting
- macOS BSD vs GNU grep
- Common pitfalls
- What to do next
- FAQ
Basic search
Find a literal string in a file.
grep 'error' app.logCase-insensitive search.
grep -i 'error' app.logSearch standard input (the most common pipeline use):
journalctl -u nginx | grep '500'Regex flavors: BRE, ERE, PCRE
grep has three regex modes. The same pattern can mean different things in each.
| Mode | Flag | What it gives you | Available on |
|---|---|---|---|
| Basic (BRE) | none (default) | Bare metacharacters: . * ^ $ [...]. +, ?, (), {} need backslashes | GNU, BSD |
| Extended (ERE) | -E | Same as BRE plus +, ?, ` | , (), ` without backslashes |
| Perl-compatible (PCRE) | -P | Full PCRE: lookaheads, lookbehinds, non-greedy *?, \d, \w, named groups | GNU only |
ERE is what most people mean by "regex" today. Reach for -E by default.
grep -E '(error|warn|fatal)' app.logPCRE for things only PCRE can express (lookbehind, \d, non-greedy):
grep -P '(?<=user=)\w+' app.logNote: PowerShell's regex engine is .NET, which supports the same constructs PCRE does. macOS BSD grep is the odd one out.
For a deeper regex tour, see the regex cheat sheet. For specific patterns: match an email address, match a URL, match a hex color code.
Common flags
The flags I reach for daily.
| Flag | Meaning |
|---|---|
-i | Case-insensitive |
-v | Invert match (lines that do NOT match) |
-c | Count matches per file |
-l | List filenames with matches (no line output) |
-L | List filenames WITHOUT matches |
-n | Prefix each line with its line number |
-H | Print filename with each match (default when searching multiple files) |
-h | Suppress filename prefix |
-w | Match whole words only |
-x | Match whole lines only |
-o | Print only the matched part, not the whole line |
-q | Quiet; exit status only (handy in if tests) |
-E | Extended regex (ERE) |
-F | Fixed strings (no regex; safer when matching ., *, etc.) |
-r / -R | Recursive (R follows symlinks) |
-F is underused. If the pattern is a literal string with regex metacharacters in it, -F is faster and avoids escaping.
grep -F '192.168.1.1' access.logRecursive search
Search a whole tree.
grep -rn 'TODO' .-r is recursive, -n prints line numbers. Add -I (capital i) to skip binary files, which both GNU and BSD support:
grep -rnI 'TODO' .Worth knowing: GNU -r does NOT follow symlinks. Use -R if you want it to. BSD -r follows symlinks by default. Behavior diverges, so if a script depends on it, be explicit with -R or -r --no-dereference.
Filtering by filename
GNU grep has --include and --exclude. BSD grep does not, so on macOS you pipe find into xargs.
grep -rn --include='*.ts' 'useState' src/Excluding a directory:
grep -rn --exclude-dir=node_modules 'process.env' .If you live on macOS and use these flags daily, brew install grep gives you GNU grep as ggrep. Alias it to grep in your shell rc and the friction goes away. See the find command cheat sheet for the find | xargs pattern in more depth.
Context lines
Show N lines after, before, or around each match. Critical for log triage.
| Flag | Meaning |
|---|---|
-A N | N lines After |
-B N | N lines Before |
-C N | N lines of Context (N before and N after) |
grep -C 3 'Exception' app.log-A, -B, -C all work on BSD and GNU. The long forms (--after-context, --before-context, --context) are GNU only.
Counting, listing, inverting
Count matches per file:
grep -c 'error' *.logList only filenames that contain a match:
grep -rl 'apiKey' .Invert: show lines that do NOT match. Useful for trimming noise:
grep -v '^#' nginx.conf | grep -v '^$'That last pipeline (drop comments, drop blank lines) is the fastest way to read a heavily-commented config file.
macOS BSD vs GNU grep
The differences that matter, in order of how often they bite.
| Feature | GNU grep | BSD grep (macOS default) |
|---|---|---|
-P (PCRE) | Supported | Not supported |
--include=PATTERN | Supported | Not supported |
--exclude=PATTERN | Supported | Not supported |
--exclude-dir=DIR | Supported | Not supported |
--color=auto | Supported | Use --color only (no value) |
--context long form | Supported | -C N short form only |
Recursive -r symlinks | Does NOT follow | Follows by default |
\d, \w, \b in -E | Treated as literal | Treated as literal |
Quick fix on macOS: install GNU grep via Homebrew and use it as ggrep, or alias it.
brew install grep
alias grep='ggrep'
alias egrep='gegrep'
alias fgrep='gfgrep'Add to ~/.zshrc (or ~/.bash_profile) and reload. Scripts you ship to teammates should not assume GNU grep is present; either invoke ggrep explicitly, or stick to the BSD-compatible subset.
Common pitfalls
1. Unquoted patterns get shell-expanded. grep *.log file — the *.log is expanded by the shell into a list of files. Always quote: grep '*.log' file.
2. Patterns starting with - are read as flags. grep -test file fails because grep tries to parse -t as a flag. Use grep -- -test file or grep -e '-test' file.
3. grep -E does not enable PCRE. ERE adds +, ?, |, (), {} but not lookaheads, \d, or non-greedy *?. Those are PCRE only (GNU -P or PowerShell).
4. BSD --color syntax differs. --color=auto works on GNU, fails on BSD grep where the value is rejected. Use bare --color on BSD, or skip the flag and let GREP_OPTIONS (deprecated) or shell aliases handle it.
5. grep -r symlink semantics differ. GNU does not follow symlinks; BSD does. If your search misses (or duplicates) files depending on the host, this is why.
6. grep exits 1 when there is no match. Useful in scripts, surprising in pipelines: set -e will abort the script if no line matches. Either disable set -e for that line or wrap with || true.
7. Multi-line patterns do not work. grep is line-oriented. To match across newlines, use pcregrep -M, ripgrep --multiline, or pipe through tr '\n' ' ' first.
8. PowerShell's Select-String returns objects, not strings. Use .Line or .Matches.Value when consuming output programmatically. findstr in cmd.exe returns text but has weaker regex; prefer Select-String for anything beyond a literal search.
What to do next
- find command cheat sheet — the natural pair for
grepwhen you need to filter by filename first. - SSH cheat sheet — for greping logs on remote hosts over SSH.
- curl cheat sheet — when the pipeline starts with an HTTP response you want to grep.
- regex cheat sheet — full regex reference for crafting the patterns you feed into
grep -E/-P. - Bash for loop and Bash while loop — looping over filenames with
grep -loutput. - External: GNU grep manual, FreeBSD grep(1), PowerShell Select-String.





