Chapter 4: Quantifiers and Greediness

Goal: Control how many times a pattern should match using quantifiers like *, +, ?, and {n,m}.


What are Quantifiers?

Quantifiers specify how many times the previous element should match. Think of them like quantity indicators in English:

English Regex Meaning
“zero or more” * 0 to unlimited
“one or more” + 1 to unlimited
“optional” ? 0 or 1
“exactly n” {n} Exactly n times
“at least n” {n,} n or more
“between n and m” {n,m} n to m times

Real-World Analogy

You need to order pizza for a group:

Pattern: /pizza+/
         "pizz" + "a" one or more times

Options:
  pizza    Match: yes (1 a)
  pizzza   Match: yes (3 a's)
  pizz     Match: no (need at least 1 a)

Pattern: /chips{0,2}/
         "chip" + "s" 0 to 2 times

Options:
  chip     Match: yes (0 s)
  chips    Match: yes (1 s)
  chipss   Match: yes (2 s)
  chipsss  Match: no (too many s)

Basic Quantifiers

* - Zero or More

Matches 0 or more occurrences:

// "a" followed by zero or more "b"s
preg_match('/ab*/', 'a');      // Match: yes (0 b's)
preg_match('/ab*/', 'ab');     // Match: yes (1 b)
preg_match('/ab*/', 'abbb');   // Match: yes (3 b's)

+ - One or More

Matches 1 or more occurrences:

// "a" followed by one or more "b"s
preg_match('/ab+/', 'a');      // Match: no (need at least 1 b)
preg_match('/ab+/', 'ab');     // Match: yes (1 b)
preg_match('/ab+/', 'abbb');   // Match: yes (3 b's)

? - Zero or One (Optional)

Matches 0 or 1 occurrence (makes something optional):

// "a" followed by optional "b"
preg_match('/ab?/', 'a');      // Match: yes (0 b)
preg_match('/ab?/', 'ab');     // Match: yes (1 b)
preg_match('/ab?/', 'abb');    // Match: yes ("ab", only 1 b)

Examples with “abbb”

  • /ab*/ matches "abbb" (a followed by zero or more b).
  • /ab+/ matches "abbb" (a followed by one or more b).
  • /ab?/ matches "ab" (a followed by an optional b).

Exact Quantifiers with Braces

Use {} for precise control:

Pattern Meaning Example Matches
{n} Exactly n times a{3} -> “aaa”
{n,} At least n times a{2,} -> “aa”, “aaa”, “aaaa”…
{n,m} Between n and m a{2,4} -> “aa”, “aaa”, “aaaa”

Examples

// Exactly 3 digits
preg_match('/\d{3}/', '123');      // Match: yes
preg_match('/\d{3}/', '12');       // Match: no (only 2)
preg_match('/\d{3}/', '1234');     // Match: yes ("123", first 3)

// At least 2 digits
preg_match('/\d{2,}/', '123');     // Match: yes
preg_match('/\d{2,}/', '1');       // Match: no (only 1)

// Between 2 and 4 digits
preg_match('/\d{2,4}/', '123');    // Match: yes
preg_match('/\d{2,4}/', '12345');  // Match: yes ("1234", first 4)

Greediness: The Default Behavior

By default, quantifiers are greedy - they match as many characters as possible:

$text = "12345";

preg_match('/\d+/', $text, $matches);
echo $matches[0];  // Output: "12345" (ALL digits!)

How Greedy Matching Works

Text: "<p>hello</p>"

  • Pattern: /<.+>/
  • < is literal.
  • . matches any character.
  • + repeats one or more times.
  • Greedy matching consumes the longest possible span, so the match is "<p>hello</p>".

What Happens When Greedy Fails: Backtracking

Greedy does not mean stubborn. If the rest of the pattern fails after a greedy quantifier has eaten too much, the engine gives characters back — one at a time, right to left — and retries. This “giving back” is called backtracking, and it is the engine’s normal way of working: with /<.+>/ on "<p>a</p>", the .+ first eats p>a</p, then gives characters back until > can match. Lazy quantifiers simply start small and grow, instead of starting big and shrinking.

Backtracking becomes a security problem only when a failing input leaves the engine exponentially many ways to split the text — that is ReDoS (Regular Expression Denial of Service), the subject of Chapter 8.


Lazy (Non-Greedy) Matching

Add ? after a quantifier to make it lazy - match as few characters as possible:

$text = "<p>hello</p>";

// Greedy: matches as much as possible
preg_match('/<.+>/', $text, $matches);
echo $matches[0];  // Output: "<p>hello</p>"

// Lazy: matches as little as possible
preg_match('/<.+?>/', $text, $matches);
echo $matches[0];  // Output: "<p>" (first tag only)

Greedy vs Lazy Comparison

Pattern Text Match Why
/.+/ “abc” “abc” Greedy: all characters
/.+?/ “abc” “a” Lazy: one character is enough
/<.+>/ “<p>x</p>” “<p>x</p>” Greedy: everything
/<.+?>/ “<p>x</p>” “<p>” Lazy: stops at first >

When to Use Lazy

// Extract content between tags (lazy)
preg_match('/<p>(.+?)<\/p>/', '<p>hello</p><p>world</p>', $matches);
echo $matches[1];  // Output: "hello" (first paragraph only)

// Get all paragraphs (need preg_match_all)
preg_match_all('/<p>(.+?)<\/p>/', '<p>hello</p><p>world</p>', $matches);
print_r($matches[1]);
// Output:
// Array
// (
//     [0] => hello
//     [1] => world
// )

Possessive Quantifiers (Performance)

Add ++, *+, ?+ to forbid backtracking (a first line of defense against ReDoS):

// Regular quantifier (can backtrack)
preg_match('/a+b/', 'aaab');  // Match: yes

// Possessive (never backtracks - faster when it matches)
preg_match('/a++b/', 'aaab');  // Match: yes (no backtracking)

A possessive quantifier keeps every character it has matched — on failure it does not give anything back. a+b is perfectly safe here (a single linear quantifier); possessive matters when a quantifier sits inside another one, as in Chapter 8.

When to Use Possessive

Scenario Pattern Benefit
Match possessive /a++b/ No backtracking
Character class /[ab]++/ Faster matching
Optional possessive /a?+b/ Prevent ReDoS

Good Patterns vs Bad Patterns

Good: Specific and Safe

// Match 1-3 digits
'/\d{1,3}/'

// Match word with optional plural
'/\w+s?/'

// Match HTML tags (lazy)
'/<[^>]+>/'

// Match with possessive (performance)
'/[a-z]++/'

Bad: Too Vague or Dangerous

// Too greedy - matches too much
'/.*/'

// Nested quantifiers - ReDoS risk!
'/(a+)+$/'

What about “unbounded” quantifiers like /x{1,}/ on user input? A single quantifier is linear, bound or not — /x{1,}/ is no more dangerous than /x{1,100}/. The danger above comes from the nesting, where the engine can split the input between two quantifiers in exponentially many ways.


Exercises

Exercise 1: Identify Matches

For each pattern, what does it match?

  1. /a*/ on “aaa”
  2. /a+/ on “aaa”
  3. /a?/ on “aaa”
  4. /a{2,3}/ on “aaaa”
// Answers:
// 1. "aaa" (zero or more a's = all a's)
// 2. "aaa" (one or more a's = all a's)
// 3. "a" (zero or one a = first a only)
// 4. "aaa" (2-3 a's = first 3 a's)

Exercise 2: Write Patterns

Write patterns that match:

  1. One or more digits
  2. 10-digit phone number
  3. Optional file extension
  4. HTML tag content (lazy)
// Solution 1
$pattern1 = '/\d+/';

// Solution 2
$pattern2 = '/\d{10}/';

// Solution 3 — the ? group makes the extension optional, and the dot
// lives only inside that group, so a trailing dot is refused
$pattern3 = '/^[\w-]+(\.[a-z]+)?$/i';
// "file"     => Match: yes
// "file.txt" => Match: yes
// "file."    => Match: no

// Not an answer: /\.[a-z]+/i — nothing optional about it; it matches
// ".xxx" anywhere in any string

// Solution 4
$pattern4 = '/>(.+?)</';

Exercise 3: Test Greedy vs Lazy

$text = "<div>hello</div>";

preg_match('/<.+>/', $text, $m);
echo "Greedy: " . $m[0] . "\n";

preg_match('/<.+?>/', $text, $m);
echo "Lazy: " . $m[0] . "\n";

Key Takeaways

  1. Quantifiers control repetition: *, +, ?, {n,m}
  2. * = 0 or more (zero or more)
  3. + = 1 or more (at least one)
  4. ? = 0 or 1 (optional)
  5. {n,m} = exact range
  6. Greedy = matches maximum (default)
  7. Lazy = matches minimum (add ?)
  8. Backtracking = how the engine retries by giving characters back
  9. Possessive = no backtracking (add ++, *+, ?+)

Common Errors

Error: Forgetting Quantifier Defaults to Greedy

$text = "123 456 789";

// Greedy by default: each \d+ takes a whole run of digits
preg_match('/\d+\s+\d+/', $text, $m);
echo $m[0];  // "123 456" (two full runs - the match ends once the pattern is satisfied)

// Lazy: each quantifier stops as soon as it can
preg_match('/\d+?\s+\d+?/', $text, $m);
echo $m[0];  // "123 4" (the trailing \d+? stops after one digit)

Error: Nested Quantifiers (ReDoS Risk)

// Dangerous: Can cause exponential backtracking
'/(a+)+$/'  // DO NOT USE!

// Safe: Use possessive quantifiers
'/(a++)+$/'   // Better
'/a+$/'       // Even better - simplify!

Error: Thinking a Bound Fixes Nesting

// Dangerous: nested quantifiers, exponential backtracking
'/(a+)+$/'

// Just as dangerous: the outer {1,100} does not cap the number of
// ways to split the input between the two quantifiers
'/(a+){1,100}$/'

// Safe: one quantifier (linear), or an inner quantifier that cannot backtrack
'/a+$/'
'/(a++)+$/'
'/(?>a+)+$/'

An upper bound like {1,100} is still useful — it limits what a match may consume, a sane length policy — but it is not a security fix. The risk comes from the nesting, and only removing the nesting or locking the inner quantifier removes it. PHPRegex confirms the verdicts above: the first two patterns are critical, the last three safe.


Recap

You now understand:

  • All quantifier types (*, +, ?, {n,m})
  • Greedy vs lazy matching
  • Backtracking and possessive quantifiers
  • Common pitfalls and fixes

Next: Chapter 5: Groups and Alternation

Edit on GitHub