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.

Two ways to read alternation. The pattern caret INC bar CHG hyphen backslash d plus splits into two whole alternatives: caret INC, or CHG hyphen digits. The pattern caret, group INC or CHG, hyphen, digits is one sequence of four parts in which only the group contains the choice. ^INC|CHG-\d+ ^INC or CHG-\d+ two whole alternatives ^(?:INC|CHG)-\d+ ^ INC or CHG - \d+ one sequence
Without a group, the bar splits the whole pattern in two, and the anchor and the digits each end up on one side. A group limits the choice to the part inside it.
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)\b is safe to extend; ^WARN|ERROR is not.
  • Longest first, or make them unambiguous with \b or 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.