Skip to content

Lesson 3 of 9 · 20m

Dialects: BRE, ERE, PCRE, JavaScript, and RE2

Why a pattern that works in regex101 silently matches nothing in sed — flavor differences, escaping layers, and leftmost-longest versus leftmost-first.

Regex has its own "works on my machine." You test a pattern on regex101 in PCRE mode, paste it into a sed one-liner in a deploy script, and it silently matches nothing. No error, no warning — the config file just doesn't get patched, and you find out in production. There is no single "regex language"; there are a dozen dialects that share a family resemblance.

The flavor map

ToolDefault flavorNotes
grep, sedPOSIX BREgrep -E / sed -E for ERE; grep -P for PCRE2 (GNU only)
awkPOSIX EREgawk adds \y word boundary, no \d
rg (ripgrep)Rust regex (linear time)rg -P switches to PCRE2 for lookarounds and backrefs
Python re, JavaScript, Java, .NETPerl-style backtrackingClose cousins with different edges
Go regexp, RE2RE2No backreferences, no lookarounds
nginx location ~PCRE~* for case-insensitive
Prometheus =~RE2, fully anchoredjob=~"api" matches only exactly api

BRE versus ERE in practice

In Basic REs, ( ) { } are literal unless escaped, and + ? aren't operators at all (GNU accepts \+ and \? as extensions). ERE flips that:

# Swap "555-0142" to "0142-555"
sed 's/\([0-9]\{3\}\)-\([0-9]\{4\}\)/\2-\1/' phones.txt     # BRE: backslash soup
sed -E 's/([0-9]{3})-([0-9]{4})/\2-\1/' phones.txt          # ERE: same thing, readable

Default to -E everywhere; it works on both GNU and BSD tools.

No \d in POSIX

grep -E '\d+' app.log does not match digits. GNU grep treats \d as a literal d (newer versions at least print warning: stray \ before d). Use [0-9] or [[:digit:]], or grep -P if you really need Perl syntax.

Leftmost-longest versus leftmost-first

Both families find the leftmost match. They disagree about which match at that position:

$ echo "abcd" | grep -oE 'ab|abcd'
abcd                     # POSIX: longest alternative wins
console.log("abcd".match(/ab|abcd/)[0]);  // "ab" — Perl-style: first alternative that works wins

Order your alternations longest-first in Perl-style engines (abcd|ab), or better, make them non-overlapping.

The escaping stack

A pattern often passes through three parsers: shell, host language, regex engine. Single-quote in the shell. Use raw strings in Python (r"\d+"). In JSON or YAML configs, every backslash doubles: "pattern": "^\\d{3}-\\d{4}$". And never build a regex from user input without escaping it:

const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const host = "api.sudosecurity.org";
console.log(new RegExp("^" + host + "$").test("apixsudosecurity.org"));           // true — the dot is a wildcard
console.log(new RegExp("^" + escapeRe(host) + "$").test("apixsudosecurity.org")); // false

Field notes

  • macOS ships BSD sed and grep: sed -i '' instead of sed -i, no grep -P, and sed -r is GNU-only. -E and -i.bak work on both.
  • On regex101, set the flavor dropdown to your actual engine before trusting the green highlight.
  • Prometheus, Grafana label filters, and many Go-based tools anchor implicitly. Add .* on both sides when you mean "contains."
  • Newer runtimes ship RegExp.escape(); until your whole fleet has it, keep the helper above.