Alternation and Precedence
Choosing between alternatives with |, how far an alternative reaches, why the order of alternatives matters in backtracking engines, and how to group them correctly.
The pipe: one alternative or another
Alternation is the regex operator
for "or": a pattern A|B matches wherever either
alternative A or alternative B matches.
You can chain as many as you like: WARN|ERROR|CRIT. It
is the regex version of searching for several words at once, the
everyday task of a NOC engineer scanning a log.
In GNU grep's default syntax, a
POSIX basic regular expression, a
plain | is an ordinary character; alternation is
written \|, and POSIX leaves it to each implementation
whether that works at all. With grep -E (extended
syntax) and in every programming language on this site, the plain
| is the operator.
printf '%s\n' '08:14 INFO link up' '08:15 WARN fan speed low' '08:16 ERROR fan failed' '08:17 DEBUG poll ok' > noc.log
grep -E 'WARN|ERROR' noc.log
echo ---
grep -c 'WARN|ERROR' noc.log
echo ---
grep 'WARN\|ERROR' noc.log
08:15 WARN fan speed low 08:16 ERROR fan failed --- 0 --- 08:15 WARN fan speed low 08:16 ERROR fan failed
The middle command counts zero lines: without -E, grep
looks for the literal text WARN|ERROR. That silent zero
is a classic reason for "but the error is definitely in the log".
import re
line = "08:17 DEBUG poll ok"
print(re.search(r"[WARN|ERROR]", line))
print(re.search(r"WARN|ERROR", line))
<re.Match object; span=(7, 8), match='E'> None
Precedence: how far an alternative reaches
Like arithmetic, where 2 + 3 × 4 means
2 + (3 × 4), regex operators have a precedence that
decides what binds to what. From strongest to weakest:
| Binds | Operator | Example | Read as |
|---|---|---|---|
| 1 (strongest) | Groups, character classes, escapes |
(ab), [0-9], \d
|
one unit |
| 2 | Quantifiers | ab+ |
a(b+) |
| 3 | Sequence (concatenation), anchors | ^INC|CHG |
(^INC)|(CHG) |
| 4 (weakest) | Alternation | |
gray|grey |
(gray)|(grey) |
Because | binds weakest, each alternative stretches as
far as it can: to the nearest enclosing parenthesis, or to the ends
of the whole pattern. Everything to the left of the bar is one
alternative, everything to the right the other. Anchors and literal
text next to the bar belong to only one side.
import re
lines = ["INC-31877 portal down", "INC", "see CHG-1042", "CHG-1042 approved"]
for line in lines:
careless = re.search(r"^INC|CHG-\d+", line)
grouped = re.search(r"^(?:INC|CHG)-\d+", line)
print(f"{line:22} careless={careless and careless.group()!s:9} grouped={grouped and grouped.group()}")
INC-31877 portal down careless=INC grouped=INC-31877 INC careless=INC grouped=None see CHG-1042 careless=CHG-1042 grouped=None CHG-1042 approved careless=CHG-1042 grouped=CHG-1042
The careless pattern accepts a bare INC and finds a
change number in the middle of a sentence, because only the left
alternative is anchored and only the right one requires digits. The
grouped version applies the anchor and the -\d+ to
both. The same trap with ^ and $ around a
whole list of alternatives is shown on
Anchors and Word Boundaries.
Grouping alternatives
Parentheses limit the reach of |: in
gr(a|e)y, the choice is between a and
e only. Which kind of parentheses you use depends on
whether you need the text:
-
(?:INC|CHG), a non-capturing group, when you only need the grouping. This is the usual choice. -
(INC|CHG)or(?P<type>INC|CHG), a capture group, when you also want to know which alternative matched. -
In GNU grep and sed basic syntax:
\(INC\|CHG\). With-E:(INC|CHG). POSIX tools have no(?:…).
const log = "INC-31877 opened; CHG-1042 approved; PRB-0311 linked";
console.log(log.match(/(?:INC|CHG)-\d+/g));
for (const m of log.matchAll(/(INC|CHG|PRB)-(\d+)/g)) {
console.log(m[1].padEnd(4), m[2]);
}
[ 'INC-31877', 'CHG-1042' ] INC 31877 CHG 1042 PRB 0311
Order matters: leftmost-first
Two alternatives can both match at the same position. Which one does
the engine report? The backtracking engines of Perl, PCRE (PHP,
grep -P), Python, JavaScript, Java and .NET use
leftmost-first matching: the engine starts at the leftmost position where any match is
possible, tries the alternatives in the order they are written, and
accepts the first one that lets the whole pattern succeed. It does
not go on to look for a longer option.
import re
text = "tariff 0.27 EUR per kWh from January 2026"
print(re.findall(r"\d+|\d+\.\d+", text))
print(re.findall(r"\d+\.\d+|\d+", text))
print(re.findall(r"Jan|January", text))
print(re.findall(r"\b(?:Jan|January)\b", text))
['0', '27', '2026'] ['0.27', '2026'] ['Jan'] ['January']
With \d+ first, the engine is satisfied with
0 and never tries \d+\.\d+; the decimal
number falls apart. Swapping the alternatives fixes it. In the last
line, Jan matches first, but then \b fails
(a u follows), so the engine backtracks and
tries January. Order only decides between alternatives
that both lead to a complete match; what comes after the group can
still force the other choice.
POSIX tools: leftmost-longest
POSIX defines a different rule, and GNU grep, sed and awk follow it: leftmost-longest matching. Among all matches that start at the leftmost possible position, the engine reports the longest, regardless of the order of the alternatives. These tools are not backtracking engines; they effectively consider all alternatives at once.
That makes this one of the clearest real differences between
flavors. The same pattern, kW|kWh, on the same text
318 kWh:
echo '318 kWh' > reading.txt
grep -o -E 'kW|kWh' reading.txt
sed -E 's/kW|kWh/[&]/' reading.txt
awk '{ sub(/kW|kWh/, "[&]"); print }' reading.txt
echo --- PCRE:
grep -o -P 'kW|kWh' reading.txt
kWh 318 [kWh] 318 [kWh] --- PCRE: kW
import re
print(re.search(r"kW|kWh", "318 kWh").group())
kW
console.log("318 kWh".match(/kW|kWh/)[0]);
kW
<?php
preg_match('/kW|kWh/', "318 kWh", $m);
echo $m[0], "\n";
kW
GNU grep with -E, sed and awk report kWh;
grep -P, Python, JavaScript and PHP report
kW. When you move a pattern from a shell script into
code (or back), alternatives where one is a prefix of another are
the first thing to check. The
Regex Flavors page has more of
these differences.
Leftmost still comes first
In both rules, where a match starts matters more than which
alternative is listed first. The engine scans from left to right and
reports the first position at which anything matches.
Wh|kWh on 318 kWh finds
kWh in every flavor: at the k, the first
alternative fails but the second succeeds, and that position comes
before the one where Wh would start.
import re
print(re.search(r"Wh|kWh", "318 kWh").group())
kWh
Writing alternatives well
-
Group them, always. Even when the alternatives
are the whole pattern today, someone will add an anchor or a
suffix tomorrow.
^(?:WARN|ERROR)\bis safe to extend;^WARN|ERRORis not. -
Longest first, or make them unambiguous with
\bor a required next character, so the result does not depend on the engine. -
Use a character class for single characters.
[,;|]is clearer than,|;|\|and lets the engine test one set instead of trying three alternatives. -
Factor out common parts when it helps reading:
INC-\d+|CHG-\d+is(?:INC|CHG)-\d+. -
Avoid overlapping alternatives under a quantifier.
In
(a|aa)+the engine can split the same text in many ways, and when the rest of the pattern fails it tries them all. That is one of the classic shapes behind catastrophic backtracking; see Performance and Catastrophic Backtracking. -
Watch for empty alternatives.
(kWh|)or a stray|at the end of a list (WARN|ERROR|) matches the empty string, which matches everywhere;grep -E 'WARN|ERROR|'prints every line.