Skip to content
VLSI Mentor

awk Part IV · Module 14 · AWK Debugging, Review, and Engineering Mastery

Regular Expression Problems

A pattern that matches nothing is annoying; a pattern that matches the wrong thing is dangerous. The difference usually comes down to one character someone assumed was literal. This chapter works through the four metacharacters responsible for almost all of it, and then the part that makes regex bugs genuinely hard to reason about: a pattern crosses up to three independent escaping layers before awk compiles it, and each layer can consume a backslash. We measure all three, including a transport difference between -v and ENVIRON that changes which record a pattern selects.

Advanced20 min readawkPart IVDebuggingRegexEscaping

Part IV · Module 14 · Chapter 14.3 · Regular Expression Problems

1. Why This Matters

Chapter 14.1 put "is the pattern matching?" at step three of the diagnostic order and said to widen the pattern until it does. This chapter is what to do once you know the pattern is the problem.

Four metacharacters account for most regex bugs in this domain — (, [, . and the anchors — and each has a characteristic symptom. Then there is the escaping, which is why a pattern that works inside a script fails when pasted into a prompt, and why the same pattern handed to awk two different ways selects two different records.

The failure mode to watch for is the one this whole module keeps circling: the count is right and the selection is wrong.

2. A Fixture That Separates Them

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > timing.rpt <<'EOF'
  slack (VIOLATED)   -0.018
  slack (MET)         0.067
  slack VIOLATED      -0.112
  data arrival time    0.238
  u_alu/sum_reg[3] -> u_alu/out_reg[0]
  net_1x2 fanout 4
  version 2.1.3 build 7
EOF
awk '{ printf "  NR=%d [%s]\n", NR, $0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 [  slack (VIOLATED)   -0.018]
  NR=2 [  slack (MET)         0.067]
  NR=3 [  slack VIOLATED      -0.112]
  NR=4 [  data arrival time    0.238]
  NR=5 [  u_alu/sum_reg[3] -> u_alu/out_reg[0]]
  NR=6 [  net_1x2 fanout 4]
  NR=7 [  version 2.1.3 build 7]

Seven records. Record 1 has a parenthesised VIOLATED and record 3 has it bare — that pair is section 3. Record 5 has square brackets, record 6 has 1x2, record 7 has 2.1.3.

Every listing below prints the matched record numbers through one idiom:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/1x2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[NR=6]

The brackets matter: an empty result prints [] rather than a blank line, so "matched nothing" and "the command did not run" never look alike.

3. Parentheses Are a Group

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /slack (VIOLATED)/   matches : %s\n" "$(awk '/slack (VIOLATED)/ { n++ } END { print n+0 }' timing.rpt)"
printf "  /slack \\(VIOLATED\\)/ matches : %s\n" "$(awk '/slack \(VIOLATED\)/ { n++ } END { print n+0 }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /slack (VIOLATED)/   matches : 1
  /slack \(VIOLATED\)/ matches : 1

One match each. A count-based check passes. Now the records:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /slack (VIOLATED)/   -> "; awk '/slack (VIOLATED)/ { printf "NR=%d [%s]\n", NR, $0 }' timing.rpt
printf "  /slack \\(VIOLATED\\)/ -> "; awk '/slack \(VIOLATED\)/ { printf "NR=%d [%s]\n", NR, $0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /slack (VIOLATED)/   -> NR=3 [  slack VIOLATED      -0.112]
  /slack \(VIOLATED\)/ -> NR=1 [  slack (VIOLATED)   -0.018]

4. Brackets Are a Character Class

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /sum_reg[3]/            : %s\n" "$(awk '/sum_reg[3]/ { n++ } END { print n+0 }' timing.rpt)"
printf "  /sum_reg\\[3\\]/          : %s\n" "$(awk '/sum_reg\[3\]/ { n++ } END { print n+0 }' timing.rpt)"
printf "  index(\$0, \"sum_reg[3]\") : %s\n" "$(awk 'index($0, "sum_reg[3]") { n++ } END { print n+0 }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /sum_reg[3]/            : 0
  /sum_reg\[3\]/          : 1
  index($0, "sum_reg[3]") : 1

Zero against one. [3] is a character class containing one character, so /sum_reg[3]/ means sum_reg3 — which the file does not contain.

This one is kinder than the parentheses: the symptom is no match rather than a wrong match, and no match gets investigated. But it is far more common in this domain, because every bit-select and array index in RTL is written reg[3], data[31:0], mem[addr] — and every one of those is a character class if it reaches a pattern unescaped.

index($0, …) is the better answer whenever the target is a fixed string. It takes no regex, needs no escaping, and Chapter 7.8 measured the case where $0 ~ "|" is a fatal error while index() simply works.

5. The Dot Matches Anything

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /1x2/ -> "; awk '/1x2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' timing.rpt
printf "  /1.2/ -> "; awk '/1.2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /1x2/ -> [NR=6]
  /1.2/ -> [NR=3 NR=6]

/1.2/ selects two records where /1x2/ selects one. The dot matched the x in net_1x2 and the . in 2.1.3 — one of those was intended, the other was not, and the pattern cannot tell them apart.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /2.1.3/   : %s\n" "$(awk '/2.1.3/ { n++ } END { print n+0 }' timing.rpt)"
printf "  /2\\.1\\.3/ : %s\n" "$(awk '/2\.1\.3/ { n++ } END { print n+0 }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /2.1.3/   : 1
  /2\.1\.3/ : 1

Here both give 1 and both are right — the over-match is latent. This file happens to contain nothing else that 2-anything-1-anything-3 would hit, so the unescaped pattern is correct today and starts matching extra records the moment a line like 2x1y3 appears. Exercise 4 makes that happen with one appended line.

That is the signature of a dot bug: invisible until the input grows. Version strings, timestamps, hierarchical instance paths and decimal numbers are all dot-rich, and an escaped dot costs one character.

6. A Missing Anchor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /VIOLATED/    -> "; awk '/VIOLATED/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' timing.rpt
printf "  /^ *slack \\(/ -> "; awk '/^ *slack \(/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /VIOLATED/    -> [NR=1 NR=3]
  /^ *slack \(/ -> [NR=1 NR=2]

Two matches each, and the sets are {1,3} against {1,2}. Again the counts agree and the selections differ.

/VIOLATED/ selects on the verdict text wherever it appears, which picks up record 3's unparenthesised form. /^ *slack \(/ selects on the record shape — a slack line whose verdict is parenthesised — which picks up record 2's MET as well.

Neither is wrong. They answer different questions, which is why Chapter 9.3's conclusion was to match the shape and then read the verdict out of a field rather than trying to express both in one pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^ *slack \(/ { v = $2; gsub(/[()]/, "", v); printf "  NR=%d verdict=%s value=%s\n", NR, v, $NF }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  NR=1 verdict=VIOLATED value=-0.018
  NR=2 verdict=MET value=0.067

Two records, shape-matched, verdict extracted rather than pattern-matched — and the choice of which to report is then an explicit if, not a property of the regex. Record 3 is excluded, correctly, because it does not have the shape the parser expects; Chapter 10.2's marker-count precondition is what reports that such a record exists.

7. Layer One: The Shell

A pattern typed at a prompt crosses up to three independent escaping layers before awk compiles it. The first is the shell, and it is worth knowing precisely how little it touches:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for seq in '\[' '\.' '\\' '\$' '\t' '\(' ; do
  printf "  typed %-4s -> shell passes [%s]\n" "$seq" "$(eval "printf '%s' \"$seq\"")"
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  typed \[   -> shell passes [\[]
  typed \.   -> shell passes [\.]
  typed \\   -> shell passes [\]
  typed \$   -> shell passes [$]
  typed \t   -> shell passes [\t]
  typed \(   -> shell passes [\(]

Inside double quotes the shell consumes exactly two of these: \\ becomes one backslash, and \$ becomes a bare $. The other four pass through untouched. The narrowness is what makes it surprising — most escapes survive, and the two that do not are precisely the ones that matter for a literal backslash and for an end-of-record anchor.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'a\\b\nab\naXb\n' > bs.txt
cat -e bs.txt | sed 's/^/  /'
printf "  /a\\\\\\\\b/ -> "; awk '/a\\b/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' bs.txt
printf "  /a\\\\b/   -> "; awk '/a\b/  { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' bs.txt
printf "  /a.b/    -> "; awk '/a.b/  { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' bs.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  a\b$
  ab$
  aXb$
  /a\\b/ -> [NR=1]
  /a\b/   -> []
  /a.b/    -> [NR=1 NR=3]

Three results. /a\\b/ matches the literal backslash on record 1. /a\b/ matches nothing at all — not a\b, and not ab either, so it is not behaving as "escaped b" or as a word boundary; in this awk it is simply a pattern that cannot match. And /a.b/ takes records 1 and 3, the dot accepting the backslash and the X.

So when the shell turns /a\\b/ into /a\b/, the pattern goes from one match to none:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cat > sq.sh <<'SH'
awk '/a\\b/ { n++ } END { printf "  single-quoted: %d match(es)\n", n+0 }' bs.txt
SH
cat > dq.sh <<'SH'
awk "/a\\b/ { n++ } END { printf \"  double-quoted: %d match(es)\n\", n+0 }" bs.txt
SH
sh sq.sh
sh dq.sh
printf "    single-quoted regex text: [%s]\n" '/a\\b/'
printf "    double-quoted regex text: [%s]\n" "/a\\b/"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  single-quoted: 1 match(es)
  double-quoted: 0 match(es)
    single-quoted regex text: [/a\\b/]
    double-quoted regex text: [/a\b/]

One match against none, from the quoting alone. The two program texts are printed underneath so nothing has to be inferred: the shell delivered /a\\b/ in one case and /a\b/ in the other.

The rule is Chapter 12.1's and it is unconditional: single-quote every awk program. It is worth restating here because the symptom differs — Chapter 12.1's double-quoted $1 produced a working program with wrong output, and this produces a pattern that matches nothing at all.

8. Layer Two: awk's String Literal

The second layer applies when the pattern lives in a string rather than between slashes — a dynamic regex:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  \$0 ~ \"sum_reg\\\\\\\\[3\\\\\\\\]\" : %s\n" "$(awk '$0 ~ "sum_reg\\[3\\]" { n++ } END { print n+0 }' timing.rpt)"
printf "  \$0 ~ \"sum_reg\\\\[3\\\\]\"   : %s\n" "$(awk '$0 ~ "sum_reg\[3\]" { n++ } END { print n+0 }' timing.rpt)"
printf "  /sum_reg\\\\[3\\\\]/        : %s\n" "$(awk '/sum_reg\[3\]/ { n++ } END { print n+0 }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  $0 ~ "sum_reg\\[3\\]" : 1
  $0 ~ "sum_reg\[3\]"   : 0
  /sum_reg\[3\]/        : 1

The string form needs two backslashes where the slash form needs one. awk processes the string literal first — turning \\[ into \[ — and only then compiles what remains as a regex. Written with one backslash, the string literal consumes it, the regex compiler sees a bare [, and the pattern is a character class again.

9. Layer Three: The Transport

The third layer is how the pattern gets from the shell into the program, and it is the one that is almost never documented. Compare -v against ENVIRON on identical typed text:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for p in 'slack \(VIOLATED\)' 'slack \\(VIOLATED\\)'; do
  v=$(awk -v q="$p" '$0 ~ q { s = s sep NR; sep = "," } END { print (s == "" ? "-" : s) }' timing.rpt)
  e=$(RXP="$p" awk '$0 ~ ENVIRON["RXP"] { s = s sep NR; sep = "," } END { print (s == "" ? "-" : s) }' timing.rpt)
  printf "  typed %-22s  -v selects %-4s  ENVIRON selects %s\n" "[$p]" "$v" "$e"
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  typed [slack \(VIOLATED\)]    -v selects 3     ENVIRON selects 1
  typed [slack \\(VIOLATED\\)]  -v selects 1     ENVIRON selects -

All three layers together, with the escaping each needs for one literal [:

where the pattern is writtenwhat to typelayers crossed
/…/ in a single-quoted program\[the regex parser only
"…" in a single-quoted program\\[string literal, then regex
-v p=… from a single-quoted program\\[-v escape processing, then regex
ENVIRON["P"] from an exported variable\[the regex parser only
/…/ in a double-quoted program\[the shell passes \[ unchanged
"…" in a double-quoted program\\\\[shell, string literal, regex

The last row is the argument for never getting there. The two rows worth memorising are the first and the fourth: a slash-delimited pattern in a single-quoted program, or an exported variable read through ENVIRON, both take exactly one backslash — and those two are the only forms in this table that agree with each other.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -v pat='sum_reg\\[3\\]' '$0 ~ pat { n++ } END { printf "  -v, doubled      : %d\n", n+0 }' timing.rpt
RXP='sum_reg\[3\]' awk '$0 ~ ENVIRON["RXP"] { n++ } END { printf "  ENVIRON, single  : %d\n", n+0 }' timing.rpt
awk -v want='sum_reg[3]' 'index($0, want) { n++ } END { printf "  -v with index()  : %d\n", n+0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  -v, doubled      : 1
  ENVIRON, single  : 1
  -v with index()  : 1

Three transports, three correct answers, three different escapings — and the third needs none at all, because index() is not a regex.

10. Common Mistakes

Unescaped parentheses. /slack (VIOLATED)/ selected a different record than the escaped form — section 3.

Comparing match counts. Both forms matched exactly one record; only the record differed. Print NR.

Unescaped brackets. [3] is a character class, so the pattern became sum_reg3 and matched nothing.

A regex where a fixed string was meant. index() needs no escaping and cannot be a fatal regex.

An unescaped dot. Latent: correct today, over-matching as soon as the input grows — section 5 and Exercise 4.

Expressing the record shape and the value in one pattern. Shape-match, then read the field — section 6.

Double-quoting the program. The shell eats \\ and \$; /a\\b/ arrived as /a\b/ and matched nothing.

One backslash in a dynamic regex. The string literal consumes it; the two-layer form is "…\\[…".

Assuming -v and ENVIRON are interchangeable for a pattern. They differ by one backslash layer and select different records — section 9.

Doubling the backslashes for ENVIRON. That is the -v escaping; through ENVIRON it matches nothing.

A triple-escaped pattern in a double-quoted program. Nothing needs this. Rewrite the quoting.

11. Exercises

Exercise 1 — Shape-match, then extract

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^ *slack \(/ { v = $2; gsub(/[()]/, "", v); printf "  %-10s %8s\n", v, $NF }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  VIOLATED     -0.018
  MET           0.067

Exercise 2 — Escape a bit-select three ways

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  /sum_reg[3]/            : %s\n" "$(awk '/sum_reg[3]/ { n++ } END { print n+0 }' timing.rpt)"
printf "  /sum_reg\\[3\\]/          : %s\n" "$(awk '/sum_reg\[3\]/ { n++ } END { print n+0 }' timing.rpt)"
printf "  index(\$0, \"sum_reg[3]\") : %s\n" "$(awk 'index($0, "sum_reg[3]") { n++ } END { print n+0 }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  /sum_reg[3]/            : 0
  /sum_reg\[3\]/          : 1
  index($0, "sum_reg[3]") : 1

The first is the bug, the second is the fix, the third is the version that cannot acquire the bug again when someone edits the index.

Exercise 3 — Two rules in one program report on different records

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  one program, two rules : "
awk '/slack (VIOLATED)/ { printf "[unescaped=NR%d]", NR } /slack \(VIOLATED\)/ { printf "[escaped=NR%d]", NR } END { print "" }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one program, two rules : [escaped=NR1][unescaped=NR3]

Read the order. The escaped label comes first, because awk evaluates both rules against record 1, where only the escaped pattern matches; the unescaped rule fires later, on record 3. The output is in record order, not rule order, so the labels are what identify them — a positional reading of this line gets both backwards.

Running them separately removes the ambiguity entirely:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "  separate: unescaped -> "; awk '/slack (VIOLATED)/ { printf "NR=%d\n", NR }' timing.rpt
printf "  separate: escaped   -> "; awk '/slack \(VIOLATED\)/ { printf "NR=%d\n", NR }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  separate: unescaped -> NR=3
  separate: escaped   -> NR=1

NR=3 against NR=1 — the section 3 result, obtained without having to reason about interleaving. When comparing two patterns, run two commands.

Exercise 4 — Make a latent dot bug fire

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cp timing.rpt t2.rpt
printf "  before  /1.2/  -> "; awk '/1.2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' t2.rpt
printf 'net_1y2 fanout 9\n' >> t2.rpt
printf "  after   /1.2/  -> "; awk '/1.2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' t2.rpt
printf "  after   /1\\.2/ -> "; awk '/1\.2/ { s = s sep "NR=" NR; sep = " " } END { printf "[%s]\n", s }' t2.rpt
printf "  timing.rpt is still %s records\n" "$(awk 'END { print NR }' timing.rpt)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  before  /1.2/  -> [NR=3 NR=6]
  after   /1.2/  -> [NR=3 NR=6 NR=8]
  after   /1\.2/ -> []
  timing.rpt is still 7 records

Appending one record moved the unescaped pattern from two matches to three. The escaped /1\.2/ matches nothing — which is the correct answer, because no record contains the literal text 1.2.

Note that the work happens on t2.rpt. A chapter's fixture is shared by every later command, so a demonstration that modifies the input belongs on a copy — the final line is the check that it was.

Exercise 5 — Engineering application

Azvya Education Pvt. Ltd.VLSI Mentor
rxprobe.sh — synthetic teaching script.
#!/bin/sh
# rxprobe.sh — show what a pattern actually selects, before trusting it.
#
# Usage:  sh rxprobe.sh <file> <pattern>
#
# Prints the records the pattern selected, plus a count of its unescaped
# metacharacters -- because a count of MATCHES cannot tell you whether the
# right records were selected (section 3).
#
# The pattern reaches awk through ENVIRON, never -v: section 9 measures that
# -v performs escape processing on the value and ENVIRON does not, so a
# pattern typed for /.../ works here unchanged and would be wrong via -v.

f=$1
p=$2
[ -r "$f" ] || { echo "UNREADABLE: $f" >&2; exit 2; }
[ -n "$p" ] || { echo "usage: rxprobe.sh <file> <pattern>" >&2; exit 2; }

printf 'pattern: [%s]\n' "$p"
printf 'unescaped metacharacters: '
RXP="$p" awk 'BEGIN {
    p = ENVIRON["RXP"]
    n = 0
    for (i = 1; i <= length(p); i++) {
        c = substr(p, i, 1)
        if (c == "\\") { i++; continue }
        if (index("([.*+?{|^$", c)) { n++; m = m " " c }
    }
    printf "%d%s\n", n, (n ? " ->" m : "")
}'

printf 'matches:\n'
RXP="$p" awk '$0 ~ ENVIRON["RXP"] { n++; printf "  NR=%d [%s]\n", NR, $0 }
    END { printf "%d of %d record(s)\n", n+0, NR }' "$f"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh rxprobe.sh timing.rpt 'slack \(VIOLATED\)'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pattern: [slack \(VIOLATED\)]
unescaped metacharacters: 0
matches:
  NR=1 [  slack (VIOLATED)   -0.018]
1 of 7 record(s)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh rxprobe.sh timing.rpt 'slack (VIOLATED)'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pattern: [slack (VIOLATED)]
unescaped metacharacters: 1 -> (
matches:
  NR=3 [  slack VIOLATED      -0.112]
1 of 7 record(s)

Two runs, both reporting 1 of 7 — and the records are NR=1 and NR=3. The metacharacter count is the warning; the printed record is the evidence. Neither alone suffices, because the count of matches is identical in both runs.

The anchored pattern shows the counter reporting metacharacters that are entirely deliberate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh rxprobe.sh timing.rpt '^ *slack \('
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pattern: [^ *slack \(]
unescaped metacharacters: 2 -> ^ *
matches:
  NR=1 [  slack (VIOLATED)   -0.018]
  NR=2 [  slack (MET)         0.067]
2 of 7 record(s)

^ and * are counted, and both belong there. The count is a prompt to look, not a verdict — which is why the records are printed beneath it rather than replaced by a score.

And the refusals, following Chapter 12.6's grading: a condition under which nothing can be concluded exits 2, never a clean zero.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh rxprobe.sh nosuch.rpt 'x' 2>&1 ; echo "  missing file rc=$?"
sh rxprobe.sh timing.rpt '' 2>&1 ; echo "  empty pattern rc=$?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
UNREADABLE: nosuch.rpt
  missing file rc=2
usage: rxprobe.sh <file> <pattern>
  empty pattern rc=2

The empty pattern is worth the guard: $0 ~ "" is true for every record, so without it the tool would confidently report that the pattern matches all seven.

Exercise 6 — Challenge: show that no property of the pattern alone identifies the bug

Section 3's two patterns differ by two characters and select different records. Show that neither the match count, nor the pattern length, nor the unescaped-metacharacter count distinguishes a correct pattern from an incorrect one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for p in 'slack \(VIOLATED\)' 'slack (VIOLATED)' 'sum_reg\[3\]' 'sum_reg[3]' '1\.2' '1.2'; do
  recs=$(RXP="$p" awk '$0 ~ ENVIRON["RXP"] { s = s sep NR; sep = "," } END { print (s == "" ? "-" : s) }' timing.rpt)
  c=$(RXP="$p" awk '$0 ~ ENVIRON["RXP"] { n++ } END { print n+0 }' timing.rpt)
  m=$(RXP="$p" awk 'BEGIN { p = ENVIRON["RXP"]; n = 0
      for (i = 1; i <= length(p); i++) { c = substr(p, i, 1)
        if (c == "\\") { i++; continue }
        if (index("([.*+?{|^$", c)) n++ }
      print n }')
  printf "  %-20s matches=%-3s records=%-6s meta=%-3s length=%s\n" "$p" "$c" "$recs" "$m" "${#p}"
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  slack \(VIOLATED\)   matches=1   records=1      meta=0   length=18
  slack (VIOLATED)     matches=1   records=3      meta=1   length=16
  sum_reg\[3\]         matches=1   records=5      meta=0   length=12
  sum_reg[3]           matches=0   records=-      meta=1   length=10
  1\.2                 matches=0   records=-      meta=0   length=4
  1.2                  matches=2   records=3,6    meta=1   length=3

Read the columns against each other:

  • Match count is useless. Rows 1 and 2 both read matches=1 and select records 1 and 3. Worse, row 5 — 1\.2, the correct escaped pattern — matches zero records while the buggy 1.2 on row 6 matches two. A reviewer rewarding higher match counts would choose every wrong pattern in the table.
  • A zero metacharacter count does not mean correct. Row 5 has meta=0 and matches nothing.
  • A non-zero metacharacter count does not mean wrong. Exercise 5's ^ *slack \( scored 2 and both were deliberate.
  • Length says nothing. The escaped forms are longer in rows 1 and 3 and the shortest pattern in the table is the buggiest.

The only column that identifies the bug is records — and it is the only one that is not a summary. That is what rxprobe.sh prints and what section 3's two-command comparison establishes.

This is the fifth time in Part IV that a count has agreed while the selection differed: Chapter 12.2's '2+4' across grep and awk, Chapter 13.1's signature table, Chapter 13.2's four (check, clock) groups, Chapter 14.2's empty $2, and this. A count summarises a set and cannot identify it. Every one of those chapters ended with the same instruction, and so does this one: print the records.

12. Summary

  • ( and ) are a group. /slack (VIOLATED)/ and /slack \(VIOLATED\)/ each matched exactly one record — records 3 and 1.
  • Compare matched records, not match counts. One print NR is the whole check, and two patterns need two commands.
  • [ and ] are a character class. /sum_reg[3]/ means sum_reg3 and matched nothing — and every RTL bit-select is written this way.
  • index() for a fixed string — no escaping, and it cannot become a fatal regex.
  • The dot is latent. /1.2/ matched two records and three after one line was appended; /1\.2/ matches none, correctly.
  • An anchor changes the set, not its size. /VIOLATED/ gave {1,3}, /^ *slack \(/ gave {1,2}.
  • Shape-match, then read the field — Chapter 9.3's rule, and why Chapter 10.3 strips parentheses instead of matching them.
  • Layer one, the shell: double quotes consume \\ and \$ and pass \[ \. \t \( through. /a\\b/ arrived as /a\b/, which matches nothing at all here.
  • Layer two, the string literal: a dynamic regex needs doubled backslashes, because the literal consumes one layer before the regex compiler sees anything.
  • Layer three, the transport: -v consumes one backslash layer and ENVIRON consumes none. The same typed pattern selected record 3 through -v and record 1 through ENVIRON; no single escaping is correct for both.
  • -v for a value, ENVIRON for a pattern — and -v is still the answer to injection (Chapter 12.5) when what travels is data.
  • No property of the pattern alone identifies the bug — not the count, not the length, not the metacharacter count. Only the records.

Chapter 14.4 — Handling Malformed Reports takes the input nobody promised you: CRLF line endings, blank and comment lines, a report truncated by a crashed tool, and a column that is sometimes absent.

Where this fits

Part of the awk curriculum.