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.
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" : "-");
}SUBJECT _* _+ _? _{2,3}
drv yes - yes -
drv_ yes yes yes -
drv__ yes yes - yes
drv___ yes yes - yesThe table is the whole definition:
*— none or more. Matched all four, including the baredrvwith no underscore at all.+— one or more. Matched everything exceptdrv.?— none or one. Matcheddrvanddrv_, and rejected the two longer ones.{2,3}— between two and three. Matched onlydrv__anddrv___.
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.
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" : "-");
}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.
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");
}FIELD ^\d*$ (star) ^\d+$ (plus)
[42] accept accept
[0.42] reject reject
[] accept reject
[ns] reject rejectThe 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:
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";[anything at all] /^.*$/ : match
[] /^.*$/ : match
[0] /^.*$/ : match
so a 'validation' pattern of .* validates nothingThree 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.
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;
}paths 7 -> 7
paths 120 -> 120
paths 4096 -> 4096
paths 1000000 -> 1000000One 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.
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");/^ab+$/ on 'abab' : no match
/^ab+$/ on 'abbb' : match
/^(?:ab)+$/ on 'abab' : match
/^(?:ab)+$/ on 'abbb' : no matchThe 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.
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");
}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.
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" : "-");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.
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;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 4The 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 throughmeans 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.
