Lesson 5 of 9 · 20m
Lookarounds: Matching on Context Without Consuming It
Lookahead and lookbehind as zero-width assertions, engine support differences, and when a lookaround is cleverness you should replace with code.
Lookarounds let a pattern ask "what's next to me?" without including the answer in the match. They're the most powerful construct in Perl-style regex and the most abused. Used well, they make a replace operation surgical. Used badly, they turn a validation rule into an unreadable wall that nobody dares touch.
Four assertions, zero width
X(?=Y)— positive lookahead: X, only if followed by YX(?!Y)— negative lookahead: X, only if not followed by Y(?<=Y)X— positive lookbehind: X, only if preceded by Y(?<!Y)X— negative lookbehind: X, only if not preceded by Y
"Zero-width" is the whole idea: the engine checks the condition, then returns to the exact position it started from. Nothing is consumed and nothing ends up in match[0].
The cursor doesn't move
const withCommas = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
console.log(withCommas(1234567)); // 1,234,567
console.log(withCommas(999)); // 999Read it as: "a non-boundary position, followed by one or more groups of exactly three digits, followed by a non-digit." The match is an empty string at each insertion point; the replacement inserts a comma there. A good party trick. In production, use Intl.NumberFormat, which also knows that India groups digits as 12,34,567.
Lookbehind for surgical replacement
const log = "POST /login user=alice password=hunter2&remember=1";
console.log(log.replace(/(?<=password=)[^&\s]+/g, "[REDACTED]"));
// POST /login user=alice password=[REDACTED]&remember=1Without lookbehind you'd capture password= and re-insert it with $1. The lookbehind version says what you mean: replace the value, keep the key.
Support varies more than anything else in regex
- JavaScript (ES2018+): arbitrary-length lookbehind,
(?<=Bearer\s+)\S+works. - Python
re: lookbehind must be fixed width. The same pattern raisesre.error: look-behind requires fixed-width pattern. - PCRE and Java: bounded-length lookbehind only.
- RE2, Go,
grep -E, ripgrep's default engine, Prometheus: no lookarounds at all. Reach forgrep -Porrg -P.
The password-policy trap
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{12,64}$It works, and it's bad engineering. The user gets one "invalid password" message instead of "add a digit." Each (?=.*X) rescans the input from the start. And NIST SP 800-63B discourages composition rules entirely in favor of length and breach-list checks. Four separate test() calls give you better UX, better errors, and code your successor can read.
Field notes
- Lookarounds are atomic in Perl-style engines: once satisfied, the engine won't backtrack into them to try alternatives. Captures inside a lookahead are frozen at first success.
- "Lines not containing X" is
grep -v X, not^(?!.*X).*$. Pipeline beats pattern. - Lookarounds can match at overlapping positions, because nothing is consumed. Useful for counting overlapping occurrences, surprising everywhere else.
- If a pattern needs three lookaheads to express a business rule, it wants to be three lines of code.