Perl Part I · Module 11 · Regular Expressions
Debugging a Pattern That Lies to You
A failing pattern is unusually hard to debug because it gives you almost nothing to work with. There is no stack trace, no line number, no error — just a false where you wanted a true, or a value that is subtly not the value you asked for. The instinct is to stare at the pattern and add modifiers until something changes, and that is close to the worst available strategy, because two of the four common causes are not in the pattern at all. This chapter replaces staring with a procedure. Each step is a measurement that eliminates one class of cause: whether the match succeeded at all, whether it succeeded too generously, which component of the pattern is the one that fails, and whether the characters in the input are the characters you believe are there. Every tool it uses was built earlier in the module, which is deliberate — the diagnosis needs no new syntax, only the discipline of measuring instead of guessing.
Advanced13 min readPerlPart IRegular Expressions
Part I · Module 11 · Chapter 11.21 · Debugging a Pattern That Lies to You
1. The Procedure
Before the individual techniques, the order to apply them in. Working through these in sequence is what stops you changing the pattern before you know what is wrong with it.
Diagnosing a pattern that will not match
The two steps most often skipped are the last two, and they are the ones that find the causes the pattern cannot explain. A pattern is a hypothesis about the shape of your input; when it fails, the input is as likely to be wrong as the hypothesis.
2. The Four Failure Modes, and a Tab-Proof Pattern
Here is one rule written two ways, against six lines.
use strict;
use warnings;
# Two patterns for the same rule: one uses \s+, one uses a literal space.
my $robust = qr{^(\S+)\s+(-?\d+\.\d+)\s+ns$};
my $brittle = qr{^(\S+) (-?\d+\.\d+) ns$};
my @cases = (
[ "u_alu/A -0.12 ns", "the expected shape" ],
[ "u_alu/A\t-0.12\tns", "tabs instead of spaces" ],
[ "u_alu/A -0.12 ns", "two spaces" ],
[ "u_alu/A -0.12 ps", "unit changed" ],
[ "u_alu/A -.12 ns", "number shape changed" ],
[ "u_alu/A -0.12 ns ", "trailing whitespace" ],
);
printf "%-24s %-10s %s\n", "LINE", "\\s+ patt", "literal-space patt";
for my $c (@cases) {
my ($line, $why) = @$c;
printf "%-24s %-10s %s\n", $why,
($line =~ $robust ? "match" : "no match"),
($line =~ $brittle ? "match" : "no match");
}LINE \s+ patt literal-space patt
the expected shape match match
tabs instead of spaces match no match
two spaces match no match
unit changed no match no match
number shape changed no match no match
trailing whitespace no match no matchThe two columns differ on exactly two rows, and those two rows are the most valuable advice in the chapter.
The \s+ pattern survived the tabs and the double spaces. The literal-space pattern failed both. This is why Chapter 11.5 argued for \s+ between fields: not as a style preference, but because a tool changing its column alignment — or one row being tab-aligned where the rest are spaced — is a thing that genuinely happens, and \s+ is immune to it.
So of the registry's four failure modes, the tab one belongs specifically to patterns written with literal spaces. If your pattern already uses \s+, you can eliminate that cause without testing it.
The other three rows failed for both patterns, and each needs a different diagnosis:
- unit changed (
psnotns) — the input is a legitimate row your pattern was not written for. - number shape changed (
-.12with no leading zero) — the input is the same field in a different notation. - trailing whitespace — the input is correct and the
$anchor is tighter than reality. Chapter 11.8'schompadvice, or\s*$.
Note the last one especially. Nothing is wrong with that line; the pattern is wrong to insist that ns is the final character.
3. Did It Match At All?
The first measurement separates the two top-level cases, because they need opposite responses.
use strict;
use warnings;
# Failure mode 1: it matched, but not what you meant. Print $& and the captures.
my $line = "set_load 0.4 [get_pins u_alu/A] [get_pins u_fifo/B]";
if (my ($inner) = $line =~ /\[(.*)\]/) {
printf "captured : [%s]\n", $inner;
printf "length : %d\n", length $inner;
printf "contains a ']' : %s <- the tell\n",
($inner =~ /\]/ ? "YES" : "no");
}captured : [get_pins u_alu/A] [get_pins u_fifo/B]
length : 36
contains a ']' : YES <- the tellThis is Chapter 11.7's greedy quantifier, turned into a diagnostic. The match succeeded, so there is nothing to debug in the sense of "why does it not work" — the bug is that it worked too well.
The last line is the mechanical test, and it generalises: a captured field that contains the delimiter you used to end it proves the quantifier over-reached. You do not have to reason about backtracking; you just ask the captured text whether it contains your delimiter. If it does, replace .* with .*? or a negated class and re-measure.
The matching diagnostic for a field that came out too short is length. A 36-character "instance name" is as suspicious as one containing a bracket.
4. Bisect the Pattern
When nothing matches, the question is which component is responsible, and guessing is unnecessary.
use strict;
use warnings;
# Bisect the pattern: test prefixes until one stops matching.
my $line = "u_alu/A -0.12 ps";
my @steps = (
[ '^(\S+)', 'pin' ],
[ '^(\S+)\s+', 'gap' ],
[ '^(\S+)\s+(-?\d+\.\d+)', 'number' ],
[ '^(\S+)\s+(-?\d+\.\d+)\s+', 'second gap' ],
[ '^(\S+)\s+(-?\d+\.\d+)\s+ns', 'unit ns' ],
[ '^(\S+)\s+(-?\d+\.\d+)\s+ns$', 'end anchor' ],
);
printf "subject: %s\n\n", $line;
for my $s (@steps) {
my ($src, $label) = @$s;
printf " %-12s %s\n", $label, ($line =~ /$src/ ? "ok" : "FAILS HERE");
}subject: u_alu/A -0.12 ps
pin ok
gap ok
number ok
second gap ok
unit ns FAILS HERE
end anchor FAILS HEREFour components matched and the fifth did not. The failure is localised to the unit, with no insight required.
The technique is to write the pattern as a list of lengthening prefixes and test each one. Because every prefix is a valid pattern, the first one that fails is the first component that cannot match — and everything before it is confirmed working, which is just as useful.
Two notes on doing this in practice. Build the prefixes as strings and interpolate them, as here, so the list can live in a loop rather than as six separate statements. And keep the anchor ^ on every prefix but leave $ off until the last, since a prefix pattern is not supposed to reach the end of the line.
5. Make the Whitespace Visible
The cause that no amount of reading the pattern will reveal.
use strict;
use warnings;
# When a line "looks right" and will not match, look at the actual characters.
my $line = "u_alu/A\t-0.12 ns";
printf "the line prints as : %s\n", $line;
printf "tabs : %d\n", ($line =~ tr/\t//);
printf "spaces : %d\n", ($line =~ tr/ //);
printf "length : %d\n", length $line;
# A visible rendering
(my $vis = $line) =~ s/\t/<TAB>/g;
$vis =~ s/ /<SP>/g;
printf "visible: %s\n", $vis;the line prints as : u_alu/A -0.12 ns
tabs : 1
spaces : 1
length : 16
visible: u_alu/A<TAB>-0.12<SP>nsThe first line of output is the problem: it prints as a perfectly ordinary row. One tab and one space, and they are indistinguishable on screen.
Two tools, both built earlier in the module. tr/\t// from Chapter 11.15 counts tabs without changing the string — the counting idiom doing exactly the job it was introduced for. And a substitution to a visible token, from Chapter 11.13, renders every whitespace character as something you can see.
Make this a habit for any line that "looks right" and will not match. It takes two lines of code and it eliminates an entire class of cause. The same approach catches a non-breaking space, a carriage return from a file written on Windows, and trailing whitespace — all of which print as nothing and all of which break an anchored pattern.
6. Count What the Parts Match
The last technique is for the fourth failure mode: a pattern that used to work and now does not, where you need to know how much changed before you can say what changed.
use strict;
use warnings;
# Before changing a pattern, count what it DOES match.
my @lines = (
"u_alu/A -0.12 ns",
"u_fifo/B 0.44 ns",
"u_regf/C -0.05 ps",
"---------------",
"u_mem/D 1.20 ns",
);
my $pin = grep { /^\S+\s/ } @lines;
my $num = grep { /\s-?\d+\.\d+\s/ } @lines;
my $ns = grep { /\sns$/ } @lines;
my $whole = grep { /^\S+\s+-?\d+\.\d+\s+ns$/ } @lines;
printf "lines : %d\n", scalar @lines;
printf "look like a row : %d\n", $pin;
printf "contain a number : %d\n", $num;
printf "end in ns : %d\n", $ns;
printf "match the whole : %d\n", $whole;lines : 5
look like a row : 4
contain a number : 4
end in ns : 3
match the whole : 3Five lines in, three matching the whole pattern. The counts tell you where the other two went: four look like a row, four contain a number, three end in ns.
So one line is not a row at all — the separator — and one line is a row whose unit is not ns. Neither of those is a bug in the pattern; both are facts about the input that the pattern is correctly reporting. The step from 4 to 3 at the unit is where the loss happens, and that is the number to take to whoever owns the report format.
This is the same device as the bisection in section four, applied across a corpus rather than a single line. Bisecting tells you which component fails on one line; counting tells you how many lines each component costs you. On a real report of ten thousand rows, the second is the one that tells you whether you are looking at a pattern bug or at four genuinely malformed rows.
7. What To Remember
- Do not change the pattern before you know what is wrong. Two of the four common causes are in the input, not the pattern.
- Separate no-match from wrong-match first. They need opposite fixes.
- A captured field containing its own delimiter proves greedy over-reach. Test for it mechanically rather than reasoning about backtracking;
lengthcatches the opposite error. - Bisect with lengthening prefixes. The first prefix that fails is the first component that cannot match, and everything before it is confirmed good.
- A pattern laid out with
/xis a pattern you can bisect — a second argument for Chapter 11.17. - Make the whitespace visible.
tr/\t//counts tabs; a substitution to<TAB>and<SP>shows them. A tab and a space are identical on screen and not to the engine. \s+is tab-proof; a literal space is not. Measured: the\s+pattern matched both the tab row and the double-space row that the literal-space pattern rejected.- Count what each part matches across the whole corpus. It turns "it is broken" into a number and localises the loss to one component.
- Trailing whitespace is not a malformed line. It is a
$anchor that is tighter than reality —chomp, or\s*$.
Chapter 11.22 closes the module by arguing the other side. Patterns are the wrong tool more often than this module's nineteen chapters of enthusiasm might suggest, and the alternatives — index, split, substr, and a real parser — are each better at something specific.
Where this fits
Part of the Perl curriculum.
