The fastest way to search for several patterns at once is extended-regex alternation: grep -E 'ERROR|WARN|FATAL' app.log prints every line that matches any of the three. That is the one-liner I reach for during log triage. But it is not the only way, and it is not always the right one. grep gives you three distinct mechanisms for OR-ing patterns, each with a different tradeoff around escaping, pattern length, and speed.
This page covers all three, plus the question everyone asks next: how do you AND patterns instead of OR them? (Short answer: not in one pass.)
Set your values
Set your OS, search path, and a pattern. Every grep example below updates with your values.
The one-liner
Match any line containing one of several strings with a single extended regex:
grep -E ':pattern|WARN|FATAL' app.logThe vertical bar is the alternation operator. A line matches if it contains the part before or the part after the bar. Chain as many as you need. This is the version I use for log files where I want errors, warnings, and fatals in one scan.
Three ways to OR patterns
There are three ways to tell grep "match A or B or C". They produce the same results on simple inputs but behave differently once metacharacters and pattern counts grow.
| Method | Form | Best for |
|---|---|---|
-e per pattern | grep -e 'A' -e 'B' | A handful of patterns, each potentially containing regex metacharacters |
-E alternation | `grep -E 'A | B'` |
-f from a file | grep -f patterns.txt | Long or machine-generated pattern lists |
Way 1: -e, one pattern per flag
Pass -e once for each pattern. Every -e adds an independent search pattern, and a line matches if it satisfies any of them:
grep -e ':pattern' -e 'timeout' -e 'refused' app.logThe advantage of -e is isolation. Each pattern is its own argument, so a metacharacter in one pattern cannot interact with another. It is also the only way to pass a pattern that starts with a dash without grep mistaking it for a flag.
Way 2: -E, alternation in one regex
-E switches grep into extended-regex mode, where the unescaped | means alternation. One string, multiple branches:
grep -E ':pattern|timeout|refused' app.logThis is the most compact form and it composes with the rest of regex: you can group with parentheses, anchor with ^ and $, and apply quantifiers to a whole branch. The cost is that every character in the string is now regex-significant, so a literal dot or asterisk in one of your patterns will match more than you meant.
Way 3: -f, read the patterns from a file
grep -f FILE reads patterns from a file, one per line. Each line is treated exactly like a -e pattern:
grep -f patterns.txt app.logA patterns.txt with ERROR, timeout, and refused on three separate lines behaves identically to the -e example above. The -f form is the right choice when the list is long, when it is generated by another command, or when you want to keep the pattern set under version control. A common pipeline is to build the file on the fly:
cut -d, -f1 blocked-ips.csv > patterns.txt
grep -F -f patterns.txt access.logYou can also feed patterns through process substitution and skip the temp file entirely: grep -f <(cut -d, -f1 blocked-ips.csv) access.log.
-e vs -E: which to pick
This is the distinction that trips people up, because both flags look like they do the same job.
-e PATTERNadds one search pattern. It does not change the regex flavor. Under-ealone you are still in basic-regex mode, so|is a literal pipe character, not alternation. Use-ewhen you have a small set of distinct patterns and at least one of them contains regex metacharacters you want kept separate.-Echanges the regex flavor to extended, which makes|mean alternation inside a single pattern. Use-Ewhen you want one regex with branches.
They combine. grep -E -e 'A|B' -e 'C|D' file is valid: extended mode is on, and you have two -e patterns each containing alternation. In practice I use -e when the patterns are literal-ish and -E when I genuinely want a regex.
One concrete reason -e is safer: if a pattern contains a . or * you want treated literally, keeping it in its own -e and pairing with -F (next section) avoids escaping entirely.
-F for fixed-string multi-pattern
If none of your patterns are regex, -F is the fastest option. It tells grep to treat every pattern as a literal string, no regex parsing at all:
grep -F -e '192.168.1.1' -e '10.0.0.5' access.log-F combines with both -e and -f. The classic high-volume use is grep -F -f against a large literal list, for example matching access-log lines against a blocklist of IP addresses or domains. Because there is no regex engine involved, GNU grep -F is dramatically faster on big inputs, and you never have to escape the dots in an IP address.
grep does not do AND in one pass
Every method above is OR. There is no grep flag that means "line contains A and B". grep matches a line if the pattern matches anywhere in it, and alternation is the only multi-pattern operator. To require all of several patterns, you have two options.
Chain greps. Each grep in the pipe filters the survivors of the previous one. Order does not affect the result, but putting the most selective pattern first is faster:
grep ':pattern' app.log | grep 'user=42'That returns only lines containing both :pattern and user=42. This is the portable, BSD-safe answer and the one I use most.
PCRE lookahead. GNU grep -P supports zero-width lookahead, which lets you express AND in a single pattern. Each (?=.*X) asserts "X appears somewhere ahead" without consuming input, so stacking them requires every term:
grep -P '(?=.*ERROR)(?=.*user=42)' app.logThis is single-pass and reads cleanly once you know the idiom, but -P is GNU only. It does not exist on macOS BSD grep. If a script needs to run on both Linux and macOS, chain greps instead, or install GNU grep with brew install grep and call ggrep.
Multiple patterns with other flags
OR-ing patterns composes with the rest of the flag set.
Recursive search across a tree, any of several patterns:
grep -rnE ':pattern|TODO|FIXME' :search_pathCount how many lines match any pattern with -c:
grep -cE ':pattern|WARN|FATAL' app.logList only the filenames that contain a match, no line output, with -l:
grep -rlE ':pattern|panic|fatal' :search_path-c counts matching lines, not total matches, so a line with two of your patterns still counts once. For a frequency breakdown, pipe grep -oE into sort | uniq -c instead.
macOS BSD grep vs GNU grep
The mechanisms above mostly work on both, with one important exception.
| Feature | GNU grep | BSD grep (macOS default) |
|---|---|---|
-e (repeatable pattern) | Supported | Supported |
-E (extended regex / alternation) | Supported | Supported |
-f FILE (patterns from file) | Supported | Supported |
-F (fixed strings) | Supported | Supported |
-P (PCRE, needed for lookahead AND) | Supported | Not supported |
--include / --exclude-dir | Supported | Not supported |
| Chained greps in a pipe | Works | Works |
The takeaway: OR-ing patterns is fully portable. AND via grep -P '(?=.*A)(?=.*B)' is GNU only. On macOS, either chain greps or brew install grep and use ggrep. See the grep cheat sheet for the full BSD vs GNU divergence list.
Common mistakes
1. Forgetting -E, so | is literal. grep 'ERROR|WARN' app.log without -E searches for the seven-character literal string ERROR|WARN, which matches nothing. In basic-regex mode the bar is just a character. Either add -E, or escape the bar as a basic-regex alternation with a backslash, or use repeated -e flags.
2. Regex metacharacters in -e patterns. -e does not make a pattern literal, it only isolates it. A pattern like app.log passed to -e still treats the dot as "any character". If you want literal text, add -F, or escape the metacharacters.
3. Confusing AND with OR. grep -E 'A|B' is OR, it matches a line with either. There is no single-pattern AND. If you wanted "lines with both A and B", you need chained greps or grep -P lookahead. This is the single most common misunderstanding with multi-pattern grep.
4. Trailing blank line in a -f file. An empty line in patterns.txt is an empty pattern, and an empty pattern matches every line. Your "filtered" output is suddenly the entire file. Strip blank lines from generated pattern files: grep -v '^$' raw.txt > patterns.txt.
5. Patterns that start with a dash. grep -E '-v|x' file makes grep try to parse -v as a flag. Use grep -E -e '-v|x' file or put -- before the pattern.
6. Assuming alternation is anchored. grep -E 'cat|dog' matches any line containing cat or dog, including category and dogma. Add -w for whole words, or anchor the branches: grep -E '^(cat|dog)$'.
When NOT to use this
Multi-pattern grep is the right tool for line-oriented OR matching. Reach for something else when:
- The match spans multiple lines.
grepis line-oriented; a pattern can never match across a newline. Useripgrep --multiline,pcregrep -M, or preprocess the input. The grep cheat sheet covers the multi-line workarounds. - You are searching a large repo interactively.
ripgrep(rg) ORs patterns with the same-eand|syntax, respects.gitignore, and is much faster on big trees. For day-to-day code search,rg -e 'A' -e 'B'beatsgrep -r. - The data is structured. For JSON, YAML, or CSV, a real parser (
jq,yq, a CSV-aware tool) understands fields. Multi-pattern grep matches text and will happily match inside a string value you did not mean to touch. - You need true boolean logic with grouping. "A and (B or C) but not D" is awkward as chained greps. At that point a small
awkscript orrgwith a single PCRE2 pattern is clearer than a five-stage pipe.
See also
- grep cheat sheet: the full grep flag reference, BSD vs GNU divergences, and PowerShell equivalents
- grep regex: BRE, ERE, and PCRE explained: why alternation needs
-Eand what-Padds on top - grep invert match with -v: the opposite operation, excluding lines that match any pattern
- find files modified in the last 7 days: pair with a
findandxargspipeline to multi-pattern search inside a filtered file set - External: GNU grep manual, FreeBSD grep(1) man page





