Skip to content
VLSI Mentor

Perl Part I · Module 11 · Regular Expressions

Quantifiers: * + ? {n,m}

Character classes say what kind of character goes in a position; quantifiers say how many. That combination is what finally lets a pattern describe a shape rather than an alphabet — the gap Chapter 11.1 opened and Chapter 11.5 could not close. The four forms take about a minute to learn and a career to stop misusing, because the difference between none-or-more and one-or-more is a single character and it decides whether your number check quietly accepts an empty column. Section three measures that directly. Section five settles a question people guess at for years: what a quantifier actually attaches to, which is one item and not the string you had in mind. And section seven corrects a belief I held until the measurement contradicted it, about what a space inside the braces does.

Foundation15 min readPerlPart IRegular Expressions

Part I · Module 11 · Chapter 11.6 · Quantifiers: * + ? {n,m}

1. The Four Forms Against One Subject

Every pattern in this chapter is anchored with ^ and $ so that it tests the whole string. That is deliberate: it keeps greediness — Chapter 11.7's subject — from affecting any verdict here, because an anchored whole-string test either matches or does not, regardless of how much the quantifier tried to take first.

Azvya Education Pvt. Ltd.VLSI Mentor
four-quantifiers.pl
use strict;
use warnings;

my @subj = ("drv", "drv_", "drv__", "drv___");

printf "%-9s %-8s %-8s %-8s %s\n", "SUBJECT", "_*", "_+", "_?", "_{2,3}";
for my $s (@subj) {
    printf "%-9s %-8s %-8s %-8s %s\n", $s,
        ($s =~ /^drv_*$/     ? "yes" : "-"),
        ($s =~ /^drv_+$/     ? "yes" : "-"),
        ($s =~ /^drv_?$/     ? "yes" : "-"),
        ($s =~ /^drv_{2,3}$/ ? "yes" : "-");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
SUBJECT   _*       _+       _?       _{2,3}
drv       yes      -        yes      -
drv_      yes      yes      yes      -
drv__     yes      yes      -        yes
drv___    yes      yes      -        yes

The table is the whole definition:

  • * — none or more. Matched all four, including the bare drv with no underscore at all.
  • + — one or more. Matched everything except drv.
  • ? — none or one. Matched drv and drv_, and rejected the two longer ones.
  • {2,3} — between two and three. Matched only drv__ and drv___.

Note that * and ? agree on the first two rows and disagree on the last two. They are both "optional", but ? caps the count at one while * has no ceiling.

2. Counted Repetition

The brace form is the one you reach for when the count is part of the specification.

Azvya Education Pvt. Ltd.VLSI Mentor
counted.pl
use strict;
use warnings;

printf "%-7s %-7s %-8s %s\n", "SUBJECT", "x{3}", "x{3,}", "x{2,4}";
for my $n (1 .. 5) {
    my $s = "x" x $n;
    printf "%-7s %-7s %-8s %s\n", $s,
        ($s =~ /^x{3}$/   ? "yes" : "-"),
        ($s =~ /^x{3,}$/  ? "yes" : "-"),
        ($s =~ /^x{2,4}$/ ? "yes" : "-");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
SUBJECT x{3}    x{3,}    x{2,4}
x       -       -        -
xx      -       -        yes
xxx     yes     yes      yes
xxxx    -       yes      yes
xxxxx   -       yes      -

Three spellings, three behaviours. {3} is exactly three. {3,} is three or more, with no upper limit — note the trailing comma, without which it means exactly three. {2,4} is a closed range.

This form is how you express a real format rule. A four-digit year is \d{4}, a two-digit month is \d{2}, and a MAC-style field is [0-9a-f]{2}. Writing \d+ where the specification says four digits accepts a five-digit value, which is the sort of thing that passes every test you wrote and fails on real data.

The two forms you can now see are redundant: {0,} is *, {1,} is +, and {0,1} is ?. Use the short forms; they are what readers expect.

3. The One-Character Bug: * Accepts Nothing

This is the section to remember from the chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
star-accepts-nothing.pl
use strict;
use warnings;

# THE BUG: * allows zero, so a "is this a number" test accepts an empty field.
my @fields = ("42", "0.42", "", "ns");

printf "%-8s %-12s %s\n", "FIELD", "^\\d*\$ (star)", "^\\d+\$ (plus)";
for my $f (@fields) {
    printf "%-8s %-12s %s\n", "[$f]",
        ($f =~ /^\d*$/ ? "accept" : "reject"),
        ($f =~ /^\d+$/ ? "accept" : "reject");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
FIELD    ^\d*$ (star) ^\d+$ (plus)
[42]     accept       accept
[0.42]   reject       reject
[]       accept       reject
[ns]     reject       reject

The two patterns agree on three of the four fields and disagree on the empty one. ^\d*$ accepts the empty string, because "none or more digits" is satisfied by none.

That is not a theoretical case. An empty field is exactly what a report parser meets when a tool leaves a column unpopulated — an unconstrained path with no slack value, a cell with no derating factor, a row that the tool emitted as a placeholder. A validation written with * waves it through, and the script goes on to treat "" as a number. Perl will then numify it to zero without complaint, and a blank slack becomes a slack of 0.00, which reads as "just barely met" rather than "no data".

The degenerate version of the same mistake is worth seeing, because it appears in real scripts as a "placeholder" pattern that never got tightened:

Azvya Education Pvt. Ltd.VLSI Mentor
dot-star-always.pl
use strict;
use warnings;

# A pattern made only of * quantifiers matches ANYTHING, including "".
my @subjects = ("anything at all", "", "0");

for my $s (@subjects) {
    printf "%-18s /^.*\$/ : %s\n", "[$s]", ($s =~ /^.*$/ ? "match" : "no match");
}
print "\nso a 'validation' pattern of .* validates nothing\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
[anything at all]  /^.*$/ : match
[]                 /^.*$/ : match
[0]                /^.*$/ : match

so a 'validation' pattern of .* validates nothing

Three for three, including the empty string. /^.*$/ is always true. If you find it in a validation table it is not a loose rule, it is the absence of a rule.

4. Matching a Number Without Guessing Its Length

Now the constructive use, which is the reason the canonical description calls these out.

Azvya Education Pvt. Ltd.VLSI Mentor
unknown-length.pl
use strict;
use warnings;

# The real reason quantifiers exist: you do not know how long the field is
my @lines = ("paths 7", "paths 120", "paths 4096", "paths 1000000");

for my $l (@lines) {
    my ($n) = $l =~ /paths (\d+)/;
    printf "%-16s -> %s\n", $l, $n;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
paths 7          -> 7
paths 120        -> 120
paths 4096       -> 4096
paths 1000000    -> 1000000

One pattern, four field widths, no configuration. This is what Chapter 11.1 meant by a pattern earning its cost: \d+ is a loop that consumes digits until there are no more, and writing that loop by hand is the parser you did not want to write.

5. A Quantifier Binds to One Item

Here is the question almost everyone gets wrong at first, settled by measurement.

Azvya Education Pvt. Ltd.VLSI Mentor
binds-to-one.pl
use strict;
use warnings;

# A quantifier applies to the ONE item before it, not the whole string
printf "/^ab+\$/   on 'abab' : %s\n", ("abab" =~ /^ab+$/     ? "match" : "no match");
printf "/^ab+\$/   on 'abbb' : %s\n", ("abbb" =~ /^ab+$/     ? "match" : "no match");
printf "/^(?:ab)+\$/ on 'abab' : %s\n", ("abab" =~ /^(?:ab)+$/ ? "match" : "no match");
printf "/^(?:ab)+\$/ on 'abbb' : %s\n", ("abbb" =~ /^(?:ab)+$/ ? "match" : "no match");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
/^ab+$/   on 'abab' : no match
/^ab+$/   on 'abbb' : match
/^(?:ab)+$/ on 'abab' : match
/^(?:ab)+$/ on 'abbb' : no match

The two patterns are near-opposites, and neither means "one or more ab".

/^ab+$/ reads as: an a, then one-or-more b. So it matches abbb and refuses abab. The + attached itself to the single character before it.

/^(?:ab)+$/ is the one that means "one or more of the pair ab" — it matches abab and refuses abbb. The (?:...) groups ab into a single item so the quantifier has a two-character thing to repeat.

The lesson generalises: a quantifier always applies to exactly one item, where an item is a single character, a character class, an escape like \d, or a group. Anything longer needs parentheses. (?:...) is the non-capturing group — it does the grouping without storing what it matched, which is what you want whenever the grouping is purely structural. Chapter 11.9 covers grouping properly and Chapter 11.10 covers the capturing kind.

6. ? on a Group Makes a Whole Section Optional

Combine ? with a group and you can describe parts that may or may not be there.

Azvya Education Pvt. Ltd.VLSI Mentor
optional-group.pl
use strict;
use warnings;

# ? makes the preceding item optional -- including a whole group
my @vals = ("0.42", "42", "0.42e-9", "-0.42");

printf "%-10s %-14s %s\n", "VALUE", "\\d+(?:\\.\\d+)?", "-?\\d+(?:\\.\\d+)?";
for my $v (@vals) {
    printf "%-10s %-14s %s\n", $v,
        ($v =~ /^\d+(?:\.\d+)?$/   ? "accept" : "reject"),
        ($v =~ /^-?\d+(?:\.\d+)?$/ ? "accept" : "reject");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
VALUE      \d+(?:\.\d+)?  -?\d+(?:\.\d+)?
0.42       accept         accept
42         accept         accept
0.42e-9    reject         reject
-0.42      reject         accept

\d+(?:\.\d+)? is "digits, then optionally a dot followed by more digits" — a decimal number whose fractional part may be absent. It accepts both 0.42 and 42, and this is finally a pattern that states the shape of a number rather than its alphabet. It rejects .... and 0.4.2, which is the correctness Chapter 11.1 was reaching for.

Adding -? at the front makes the sign optional, which fixes the bug Chapter 11.1 deliberately left in: -0.42 is now accepted. In timing analysis that matters more than anywhere, because a negative number is precisely the one you care about.

Both patterns reject 0.42e-9, and that is worth stating plainly rather than hiding. Neither handles exponent notation. If your tool can emit 1.2e-09, the pattern needs another optional group — (?:[eE][-+]?\d+)? — and the honest way to decide is to look at real output from the tool you are parsing, not to guess.

7. What { } Must Contain

There is a detail about the brace form that is easy to get wrong in both directions.

Azvya Education Pvt. Ltd.VLSI Mentor
brace-contents.pl
use strict;
use warnings;

# A space inside the braces is tolerated: {2, 4} is still a quantifier.
printf "%-7s %-10s %s\n", "SUBJECT", "x{2,4}", "x{2, 4}";
for my $n (1 .. 5) {
    my $s = "x" x $n;
    printf "%-7s %-10s %s\n", $s,
        ($s =~ /^x{2,4}$/  ? "match" : "-"),
        ($s =~ /^x{2, 4}$/ ? "match" : "-");
}

print "\n";
# But braces that are not a valid count are LITERAL text (and warn on stderr).
printf "'x{2,x}' =~ /^x{2,x}\$/ : %s\n", ("x{2,x}" =~ /^x{2,x}$/ ? "match" : "-");
printf "'xx'     =~ /^x{2,x}\$/ : %s\n", ("xx"     =~ /^x{2,x}$/ ? "match" : "-");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
SUBJECT x{2,4}     x{2, 4}
x       -          -
xx      match      match
xxx     match      match
xxxx    match      match
xxxxx   -          -

'x{2,x}' =~ /^x{2,x}$/ : match
'xx'     =~ /^x{2,x}$/ : -

I expected the space to break the quantifier, and it does not. On this Perl the two columns are identical — {2, 4} is a quantifier, matching lengths 2, 3 and 4 exactly as {2,4} does, with no warning. That is worth knowing mostly so you do not waste time hunting a bug that is not there.

What does turn braces into literal text is contents that are not a valid count. /^x{2,x}$/ matched the literal six-character string x{2,x} and did not match xx. Perl is explicit about this on stderr, though the warning is not in the output block above because that block shows stdout only:

Unescaped left brace in regex is passed through in regex; marked by <-- HERE in m/^x{ <-- HERE 2,x}$/

Treat that warning as an error in your own code. It means a brace you intended as a quantifier is being matched as a bracket, and the pattern is now looking for text nobody will ever send. If you genuinely want a literal brace — matching Tcl-style set_load 0.4 {u_alu/A}, for instance — escape it as \{ or put it in a class as [{], and the warning goes away.

8. A Worked Case: Where + and a Counted Range Disagree

Section two claimed that \d+ where the specification says four digits is a real bug. Here is what that costs on rows a tool might actually emit, where the stamp is specified as YYYY-MM-DD.

Azvya Education Pvt. Ltd.VLSI Mentor
worked-counted.pl
use strict;
use warnings;

# Rows from a report where the stamp is specified as YYYY-MM-DD.
my @rows = (
    "2026-01-07 u_alu/A 0.42",
    "2026-1-7 u_alu/B 0.44",       # not zero-padded: NOT the specified format
    "20260107 u_alu/C 0.31",       # no separators at all
    "2026-01-07-u_alu/D 0.19",     # stamp runs into the pin name
);

my $loose = qr{^\d+-\d+-\d+\s};              # "some digits, dashes, some digits"
my $exact = qr{^\d{4}-\d{2}-\d{2}\s};        # the actual specification

printf "%-26s %-8s %s\n", "ROW", "loose", "exact";
my ($nl, $ne) = (0, 0);
for my $r (@rows) {
    my $l = ($r =~ $loose) ? "accept" : "reject";
    my $e = ($r =~ $exact) ? "accept" : "reject";
    $nl++ if $l eq "accept";
    $ne++ if $e eq "accept";
    printf "%-26s %-8s %s\n", $r, $l, $e;
}
printf "\nloose  ^\\d+-\\d+-\\d+\\s        accepted %d of %d\n", $nl, scalar @rows;
printf "exact  ^\\d{4}-\\d{2}-\\d{2}\\s   accepted %d of %d\n", $ne, scalar @rows;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
ROW                        loose    exact
2026-01-07 u_alu/A 0.42    accept   accept
2026-1-7 u_alu/B 0.44      accept   reject
20260107 u_alu/C 0.31      reject   reject
2026-01-07-u_alu/D 0.19    reject   reject

loose  ^\d+-\d+-\d+\s        accepted 2 of 4
exact  ^\d{4}-\d{2}-\d{2}\s   accepted 1 of 4

The loose pattern accepted two rows; the exact one accepted one. The extra row is 2026-1-7, which is not zero-padded and therefore not the specified format at all.

Whether that is a bug depends entirely on what you do next. If you are only testing "does this line start with a date", accepting it is harmless and arguably kind. But if the next step is substr($row, 0, 10) to slice the stamp out — a completely ordinary thing to write once a pattern has confirmed the shape — then on the second row you slice 2026-1-7 u and the field is silently corrupted. The counted form is what makes the slice safe, because it has guaranteed the width.

Both patterns correctly rejected the last two rows, and for different reasons worth noticing. 20260107 has no separators, so neither pattern's dashes match. 2026-01-07-u_alu/D has a perfectly good date, but the \s at the end of both patterns demands whitespace after it and finds a dash — which is the anchor-like discipline that stops a stamp from running into the next field. Both are examples of the pattern catching a malformed row precisely because it was specific.

9. What To Remember

  • * none or more, + one or more, ? none or one, {n,m} a counted range. {0,}, {1,} and {0,1} are the long spellings of the first three.
  • {3,} needs the trailing comma. Without it, {3} is exactly three.
  • ^\d*$ accepts the empty string. Use * for "may be absent", never for "must be valid" — an unpopulated column passes, and "" then numifies to zero.
  • /^.*$/ matches everything, including "". It is not a loose validation, it is no validation.
  • A quantifier binds to one item. /^ab+$/ means "a, then one-or-more b" — use (?:ab)+ for one-or-more of the pair.
  • -?\d+(?:\.\d+)? is a signed decimal and states a shape, not an alphabet. It does not do exponents; check your tool's real output before deciding you need them.
  • A space inside the braces is fine — {2, 4} is still a quantifier on Perl 5.34.
  • Braces with non-numeric contents are literal, and warn. Unescaped left brace in regex is passed through means your quantifier is being read as text.

Chapter 11.7 removes the safety net this chapter relied on. Every pattern here was anchored at both ends, so it did not matter how much a quantifier tried to swallow. Unanchored, .* takes as much as it possibly can — and that single default is responsible for more surprised debugging than any other behaviour in regular expressions.

Where this fits

Part of the Perl curriculum.