TechEarl

How to Search Multiple Patterns with grep

grep can OR several patterns three ways: -e per pattern, -E with alternation, or -f reading the list from a file. The one-liner is grep -E 'ERROR|WARN|FATAL' file. Here is when to pick each, how -F speeds up literal multi-pattern search, why grep has no single-pass AND, and the BSD vs GNU differences that bite on macOS.

Ishan KarunaratneIshan Karunaratne⏱️ 11 min readUpdated
Search multiple patterns with grep: grep -e 'A' -e 'B', grep -E 'A|B' alternation, and grep -f patterns.txt. Covers -F fixed strings, AND logic with chained greps and PCRE lookahead, and BSD vs GNU differences on macOS.

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

Try it with your own 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:

bash· Linux (GNU)
grep -E ':pattern|WARN|FATAL' app.log

The 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.

MethodFormBest for
-e per patterngrep -e 'A' -e 'B'A handful of patterns, each potentially containing regex metacharacters
-E alternation`grep -E 'AB'`
-f from a filegrep -f patterns.txtLong 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:

bash· Linux (GNU)
grep -e ':pattern' -e 'timeout' -e 'refused' app.log

The 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:

bash· Linux (GNU)
grep -E ':pattern|timeout|refused' app.log

This 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:

bash· Linux (GNU)
grep -f patterns.txt app.log

A 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:

bash
cut -d, -f1 blocked-ips.csv > patterns.txt
grep -F -f patterns.txt access.log

You 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 PATTERN adds one search pattern. It does not change the regex flavor. Under -e alone you are still in basic-regex mode, so | is a literal pipe character, not alternation. Use -e when you have a small set of distinct patterns and at least one of them contains regex metacharacters you want kept separate.
  • -E changes the regex flavor to extended, which makes | mean alternation inside a single pattern. Use -E when 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:

bash· Linux (GNU)
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:

bash· Linux (GNU)
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:

bash
grep -P '(?=.*ERROR)(?=.*user=42)' app.log

This 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:

bash· Linux (GNU)
grep -rnE ':pattern|TODO|FIXME' :search_path

Count how many lines match any pattern with -c:

bash· Linux (GNU)
grep -cE ':pattern|WARN|FATAL' app.log

List only the filenames that contain a match, no line output, with -l:

bash· Linux (GNU)
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.

FeatureGNU grepBSD grep (macOS default)
-e (repeatable pattern)SupportedSupported
-E (extended regex / alternation)SupportedSupported
-f FILE (patterns from file)SupportedSupported
-F (fixed strings)SupportedSupported
-P (PCRE, needed for lookahead AND)SupportedNot supported
--include / --exclude-dirSupportedNot supported
Chained greps in a pipeWorksWorks

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. grep is line-oriented; a pattern can never match across a newline. Use ripgrep --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 -e and | syntax, respects .gitignore, and is much faster on big trees. For day-to-day code search, rg -e 'A' -e 'B' beats grep -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 awk script or rg with a single PCRE2 pattern is clearer than a five-stage pipe.

See also

FAQ

TagsgrepRegexCLILinuxmacOSBSDShell 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

Use grep -l 'pattern' files to list only the filenames that contain a match. The inverted grep -L, the recursive grep -rl one-liner, the NUL-safe xargs pipeline for find-and-replace, and the macOS BSD vs GNU notes.

How to List Only Filenames with grep -l

grep -l prints the name of each file that contains a match and stops reading at the first hit, which makes it the fast answer to 'which files contain this string'. The lowercase -l, the inverted -L for files missing a pattern, the grep -rl one-liner, the NUL-safe xargs pipeline for find-and-replace, and the BSD vs GNU notes.

Archive every file matching a find pattern with tar. The safe find -print0 | tar --null --files-from=- one-liner, the macOS BSD tar -T difference, archiving by modification time, and gzip vs bzip2 vs xz vs zstd.

How to Archive Files Matching a find Pattern with tar

find locates the files, tar archives them. The safe pairing is find -print0 piped into tar reading a NUL-delimited list from stdin: no breakage on spaces or newlines. The flag breakdown, the macOS BSD tar vs GNU tar difference, the -exec append alternative, archiving by modification time, and the compression choices.

Use grep -w to match a whole word instead of a substring. What grep counts as a word boundary, the \b and \< \> regex equivalents, -x for whole-line match, and BSD vs GNU differences.

How to Match a Whole Word with grep -w

grep cat also matches category, concatenate, and scatter. grep -w cat matches only the standalone word. The whole-word flag, what grep counts as a word boundary, the regex equivalents with \b and \< \>, the stricter -x whole-line cousin, and the BSD vs GNU differences that bite on macOS.