Skip to content

*?, +?, ??, {n,m}? — as little as possible, one more at a time

One line: A ? after any quantifier makes it lazy: it matches the minimum first and extends one repetition at a time only when the rest of the pattern fails; all eight engines here agree on every row, and the engine that does not — POSIX, where a+? is (a+)? — is not one the driver can ask.

Level: reference · one construct, eight engines

What it means

Greedy Lazy Tries first
x* x*? zero, then one, then two…
x+ x+? one, then two…
x? x?? zero, then one
x{n,m} x{n,m}? n, then n+1…

Lazy is not "the fast one" and not "the correct one". It is the one that finds a different match when both a short and a long match are possible from the same start — and when only one match is possible, it finds the same match by a different route.

Measured

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

construct            python    perl      node      ruby      java      go        rust      fancy
greedy               <b>x</b>  <b>x</b>  <b>x</b>  <b>x</b>  <b>x</b>  <b>x</b>  <b>x</b>  <b>x</b>
lazy                 <b>       <b>       <b>       <b>       <b>       <b>       <b>       <b>
?? prefers nothing   ""        ""        ""        ""        ""        ""        ""        ""
{2,3}? stops at two  aa        aa        aa        aa        aa        aa        aa        aa
lazy at the end      name=     name=     name=     name=     name=     name=     name=     name=
lazy, anchored       name=x    name=x    name=x    name=x    name=x    name=x    name=x    name=x

Reading the table

  • Rows 1–2. <.+> takes the whole of <b>x</b>; <.+?> takes <b>. Eight engines, one answer each — this is the canonical demonstration and there is nothing to disagree about.
  • Row 3, a?? on a. All eight match the empty string. The lazy optional prefers nothing, and nothing is enough for the pattern to succeed, so the a is never consumed.
  • Row 4, a{2,3}?. Two, the minimum, in every engine.
  • Rows 5–6. name=.*? matches name= — a lazy quantifier at the very end of a pattern has nothing after it to force extension, so it always takes zero. Add $ and it takes everything up to the end, exactly as .* would have. A trailing .*? is a no-op in every engine here; a trailing .*?$ is spelled .*$.

The trap

Two. The first is performance folklore: .*? is not cheaper than .*, and on a subject where the match ends late it is more expensive, because it extends one character at a time and retries the tail at every step. The lesson measures the read counts in Java both ways.

The second is portability. POSIX engines — grep, sed, awk, C's <regex.h> — have no lazy quantifier, and a+? parses there as (a+)?, a legal pattern with a different meaning. A pattern using .*? moves from Python to sed and keeps working, wrongly. That engine is not in the table above because the probe cannot drive it; the lesson has the C program and the dated grep/sed runs.

Where it is measured in depth

See also