Skip to content
VLSI Mentor

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

A five-step procedure for diagnosing a regular expression that does not work: establish whether it matches, inspect what it captured, bisect the pattern, make the whitespace visible, and count what the parts match.Diagnosing a failing pattern1Did it match at all?Separate no-match from wrong-match before anything else2Print the captureA field holding its own delimiter means greedy over-reach3Bisect the patternTest lengthening prefixes until one stops matching4Show the whitespaceA tab and a space look identical on screen5Count what parts matchTurns broken into a number, and localises the loss
Five measurements, each eliminating one class of cause. Work through them in order: the first two separate a wrong match from no match at all, the third localises the failure to one component, and the last two examine the input rather than the pattern — which is why they are the ones people skip and the ones that find a tab where spaces were expected.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
four-failures.pl
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");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 match

The 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 (ps not ns) — the input is a legitimate row your pattern was not written for.
  • number shape changed (-.12 with 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's chomp advice, 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.

Azvya Education Pvt. Ltd.VLSI Mentor
matched-too-much.pl
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");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
captured : [get_pins u_alu/A] [get_pins u_fifo/B]
length   : 36
contains a ']' : YES   <- the tell

This 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.

Azvya Education Pvt. Ltd.VLSI Mentor
which-part-fails.pl
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");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
subject: u_alu/A -0.12 ps

  pin          ok
  gap          ok
  number       ok
  second gap   ok
  unit ns      FAILS HERE
  end anchor   FAILS HERE

Four 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.

Azvya Education Pvt. Ltd.VLSI Mentor
show-the-bytes.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
the line prints as : u_alu/A	-0.12 ns
tabs   : 1
spaces : 1
length : 16
visible: u_alu/A<TAB>-0.12<SP>ns

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
count-before-blaming.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
lines              : 5
look like a row    : 4
contain a number   : 4
end in ns          : 3
match the whole    : 3

Five 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; length catches 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 /x is 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.