Skip to content

(?=…) and (?!…) — assert what follows, consume nothing

One line: A lookahead succeeds if its sub-pattern would match at the current position and then leaves the position where it was — positive (?=…), negative (?!…) — in every engine here except Go and Rust's regex, which have no lookaround at all; and the negative form's most common use is a bug, because "not followed by X" is true one character earlier.

Level: reference · one construct, eight engines

What it means

Zero width: the assertion tests, it does not take. \d+(?= EUR) matches 25 in 25 EUR and the match ends after the 5; the EUR is checked and left alone. That is what makes lookahead useful in a replacement or an extraction — the context is verified without becoming part of the result.

A lookahead can contain groups, and the groups capture. (?=(a+))\1 is a lookahead that captures a+ and a backreference that then consumes it — which is an atomic group built out of two other features, and the way JavaScript gets one.

Measured

Verified output of kw_lookahead_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

construct              python  perl  node  ruby  java  go  rust  fancy
positive               25      25    25    25    25    -   -     25
negative, the trap     2       2     2     2     2     -   -     2
negative, fixed        no      no    no    no    no    -   -     no
lookahead can capture  aaa     aaa   aaa   aaa   aaa   -   -     aaa
at the start           ab1     ab1   ab1   ab1   ab1   -   -     ab1

Reading the table

  • Row 1. \d+(?= EUR) gives 25 in six engines. Go and Rust's regex refuse the construct entirely.
  • Row 2, the trap. \d+(?! EUR) on 25 EUR matches 2 in all six. The intent was "a number not followed by EUR"; the engine's reading is "a run of digits after which the next text is not EUR", and 2 — followed by 5 — satisfies that. Backtracking found the answer that makes the negative assertion true. Nothing was wrong with the engine.
  • Row 3, the fix. \b\d+\b(?! EUR) — a boundary so the digits cannot stop early — answers no, which is the correct answer for this subject. When the engine has no lookaround shows a replacement that is more correct than this lookahead.
  • Row 4. The capturing lookahead, (?=(a+))\1, matches aaa: the lookahead captured three as, the backreference consumed them.
  • Row 5, lookahead at the start. (?=.*\d)\w+ is the password-rule idiom — "must contain a digit" checked before the real match starts — and it works everywhere lookahead exists.

The trap

Row 2. Negative lookahead after a quantifier is the single most-reported "my regex is wrong" shape in this library's experience, and it is usually the pattern being right and the author's model being wrong. The assertion is evaluated at one position; the quantifier is free to choose that position. Anchor the quantifier's end with a boundary or a possessive/atomic form, or put the assertion before the quantifier where its meaning is fixed.

Where it is measured in depth

See also