awk Part I · Module 4 · Patterns, Conditions, and Regular Expressions
Pattern Matching
Chapter 2.3 explained how rules fire — that every rule is tried against every record and several can match at once. What it did not explain is what matching actually means, and that turns out to carry a decision you have been making implicitly since Module 1. A pattern written between slashes is tested against the whole record, so it finds its text anywhere on the line. The tilde operator tests a named field instead, so it finds its text only in the column you meant. Those two are easy to confuse, they usually agree, and when they disagree the whole-record version quietly selects rows that look right. This chapter makes the choice explicit and gives you three degrees of precision to choose between.
Foundation15 min readawkPart IPatternsRegular ExpressionsLog Analysis
Part I · Module 4 · Chapter 4.1 · Pattern Matching
1. Why This Matters
Almost every awk command in this curriculum has started with a pattern, and the patterns have all been one of two shapes: some text between slashes, or a comparison on a field. You have been choosing between them by instinct.
They differ in one respect that matters more than it looks:
A pattern between slashes is tested against the whole record. The tilde operator tests one named field.
Most of the time both give the same answer, which is exactly why the difference is worth learning deliberately — the day they disagree, the looser one does not fail. It selects extra rows that look entirely reasonable.
2. A Pattern Decides Which Records You Keep
One line of recall, since Chapter 2.3 covered the mechanics: a rule is a pattern and an action, the pattern decides whether the action runs on the current record, and a pattern with no action prints the record.
A synthetic timing report, reduced to one row per path:
u_core/u_alu setup -0.124 VIOLATED
u_core/u_regfile setup 0.081 MET
u_pad/u_io hold -0.052 VIOLATED
u_pad/u_clk hold 0.410 METawk '/VIOLATED/' timing.rptu_core/u_alu setup -0.124 VIOLATED
u_pad/u_io hold -0.052 VIOLATEDTwo records kept, two discarded. The pattern /VIOLATED/ asked a single question of each record: does this text appear anywhere in it?
That word anywhere is the whole subject of this chapter.
3. The Tilde Operator: Matching One Field
To ask the same question of one field, put the field on the left, ~ in the middle, and the pattern on the right:
awk '$1 ~ /u_pad/' timing.rptu_pad/u_io hold -0.052 VIOLATED
u_pad/u_clk hold 0.410 METRead $1 ~ /u_pad/ as "field 1 contains u_pad". The ~ is a match operator, not an assignment or a comparison — it answers true or false.
On this file the whole-record form gives the same two records:
awk '/u_pad/ { print $1 }' timing.rptu_pad/u_io
u_pad/u_clkSame answer, because u_pad only ever appears in column 1 here. That agreement is a property of the data, not of the commands.
4. Where They Disagree
Here is a report with a path whose name contains the word VIOLATED, and whose verdict is MET:
u_core/u_alu setup -0.124 VIOLATED
u_VIOLATED/u_x setup 0.900 METThe whole-record pattern:
awk '/VIOLATED/ { print $1, $4 }' tricky.rptu_core/u_alu VIOLATED
u_VIOLATED/u_x METTwo records, and the second one met its timing. The output looks like a list of violations and one of its rows is a passing path.
The field-anchored version:
awk '$4 ~ /VIOLATED/ { print $1, $4 }' tricky.rptu_core/u_alu VIOLATEDOne record, correctly. $4 names the verdict column, so the path's name is irrelevant.
5. !~ : Not Matching
The negated form selects records where the field does not match:
awk '$1 !~ /u_pad/ { print $1 }' timing.rptu_core/u_alu
u_core/u_regfileThe two paths that are not in the pad hierarchy. !~ is a single operator, not ! applied to ~, though it means the same thing. There is also a whole-record negation using ! in front of the pattern, which Chapter 4.2 covers along with the other logical operators.
6. Three Degrees of Tightness
Once you can anchor a match to a field, there are three distinct things you might mean, in increasing order of precision. All three are shown on the tricky report:
awk '/VIOLATED/ { print "loose: ", $1 }' tricky.rptloose: u_core/u_alu
loose: u_VIOLATED/u_xawk '$4 ~ /VIOLATED/ { print "field: ", $1 }' tricky.rptfield: u_core/u_aluawk '$4 == "VIOLATED" { print "exact: ", $1 }' tricky.rptexact: u_core/u_alu| Form | Asks | Selects on tricky.rpt |
|---|---|---|
/VIOLATED/ | does the text appear anywhere in the record | 2 records — one of them wrong |
$4 ~ /VIOLATED/ | does field 4 contain the text | 1 record |
$4 == "VIOLATED" | is field 4 exactly this text | 1 record |
The second and third agree here and come apart on different data. $4 ~ /VIOLATED/ would also match a verdict of NOT_VIOLATED or VIOLATED_HOLD, because contains is still a loose test within the field. $4 == "VIOLATED" demands the entire field be that string.
7. A Pattern Held in a Variable
A pattern does not have to be written literally. The right-hand side of ~ can be a variable holding a string, which awk then uses as a pattern:
awk -v pat="hold" '$2 ~ pat { print $1 }' timing.rptu_pad/u_io
u_pad/u_clkThe -v option passes a value in from the command line — Chapter 8.2 covers it properly — and here it carries the text to search for. This is what lets one script be reused for different searches without editing it.
Note there are no slashes around pat. Writing $2 ~ /pat/ would search for the literal three letters p, a, t. The slashes mean "this is a pattern written here"; a bare variable means "use what this holds as a pattern".
8. Case Sensitivity
Matching is case-sensitive, with no option to change it in POSIX awk:
awk '/violated/ { print }' timing.rptNo output. The report says VIOLATED and the pattern says violated, so nothing matched — no error, exit status 0, and the familiar problem that silence is indistinguishable from a clean report.
The portable way to accept either is a character class, which Chapter 4.4 covers:
awk '/[vV]iolated|VIOLATED/ { print $1 }' timing.rptu_core/u_alu
u_pad/u_ioThat works but is clumsy. gawk offers IGNORECASE, which is not POSIX and will be ignored by the awk these examples were verified with — Chapter 8.6 covers that split. In practice, EDA tools are consistent about their own casing, so the realistic advice is to check the report once and match what it actually says.
9. An Engineering Example
A synthetic simulation log with a severity column and free-form messages:
100 UVM_INFO Test started
250 UVM_WARNING Retry detected
420 UVM_ERROR Data mismatch
480 UVM_INFO Checking UVM_ERROR recovery
500 UVM_FATAL Bus hungThe loose form over-selects, exactly as section 4 predicted:
awk '/UVM_ERROR/ { print $1, $2 }' sim.log420 UVM_ERROR
480 UVM_INFOTwo records, and the second is informational. Printing $2 alongside makes the problem visible — the severity column says UVM_INFO.
The field-anchored form is right:
awk '$2 == "UVM_ERROR" { print $1, $3, $4 }' sim.log420 Data mismatchAnd because the severity is a closed vocabulary, == is the correct degree of tightness. The message, by contrast, genuinely needs a loose match — it is free text with no column structure:
awk '$2 == "UVM_ERROR" && /mismatch/ { print $1 }' sim.log420Severity by exact field, message content by whole-record text. Two different degrees of tightness in one rule, each chosen for the thing it is testing. (The && is Chapter 4.2's subject and is used here only to show the combination.)
The caveats are unchanged from Chapter 1.2 and remain the point of the exercise: one error record is not one bug, because a single root cause commonly prints on every subsequent transaction, and a log with no error records is not a passing run, because a tool that died during elaboration never printed one.
10. Common Mistakes
Using /text/ when you meant a column. Sections 4 and 6. The commonest cause of a report that lists plausible wrong rows.
Writing $2 ~ /pat/ when pat is a variable. Section 7. The slashes make it a literal pattern; drop them to use the variable's contents.
Expecting ~ to be an assignment. It is a match operator returning true or false. $1 = /x/ is something else entirely and almost never what you want.
Assuming ~ means equality. $4 ~ /VIOLATED/ also matches NOT_VIOLATED. Use == for a closed vocabulary.
Expecting case-insensitive matching. Section 8. POSIX awk has none; IGNORECASE is a gawk extension.
Reading no output as a clean result. A pattern that matches nothing exits 0, which Chapter 1.6 and Chapter 2.4 both warned about and which section 8 reproduces.
Forgetting that a path or a message can contain anything. Instance names, test names and message text are the fields most likely to carry a word that belongs to another column.
11. Exercises
Exercise 1 — Predict the output
u_core/u_alu setup -0.124 VIOLATED
u_core/u_regfile setup 0.081 MET
u_pad/u_io hold -0.052 VIOLATED
u_pad/u_clk hold 0.410 METawk '$2 ~ /hold/ { print $1 }' timing.rptu_pad/u_io
u_pad/u_clkThe two records whose check type is hold. On this file $2 == "hold" gives the same answer; they would differ if a check type were ever hold_min or similar.
Exercise 2 — Modify a command
Select the records whose endpoint is not in the u_core hierarchy, printing the path and verdict.
awk '$1 !~ /u_core/ { print $1, $4 }' timing.rptu_pad/u_io VIOLATED
u_pad/u_clk METExercise 3 — Trace the three degrees
For the tricky report, say how many records each of the three forms selects before running them.
u_core/u_alu setup -0.124 VIOLATED
u_VIOLATED/u_x setup 0.900 METawk '/VIOLATED/ { n++ } END { print "loose", n+0 }' tricky.rptloose 2awk '$4 ~ /VIOLATED/ { n++ } END { print "field", n+0 }' tricky.rptfield 1awk '$4 == "VIOLATED" { n++ } END { print "exact", n+0 }' tricky.rptexact 1Two, one, one. The loose form counted a passing path as a violation because the word appears in its name. If this were a nightly report, that is one phantom violation per such path — and the paths most likely to be named after a condition are exactly the ones in a test harness built to exercise it.
Exercise 4 — Debug an over-selecting filter
A colleague reports "three errors" from the log below and you believe there is one. Their command is awk '/UVM_ERROR/' sim.log. Diagnose it and give them the right command.
100 UVM_INFO Test started
250 UVM_WARNING Retry detected
420 UVM_ERROR Data mismatch
480 UVM_INFO Checking UVM_ERROR recovery
500 UVM_FATAL Bus hungawk '/UVM_ERROR/ { print $1, $2 }' sim.log420 UVM_ERROR
480 UVM_INFOTheir command matches the text anywhere, so it selected record 4 — an informational message about error recovery. Printing $2 next to it shows the severity column disagreeing with the selection, which is the quickest way to spot this class of bug.
awk '$2 == "UVM_ERROR" { print $1, $2 }' sim.log420 UVM_ERRORIf they counted three rather than two, their log has another line mentioning the text — which is itself worth looking at, and a reason to print the matches rather than only count them.
Exercise 5 — Engineering application
From the log, print the time and message of every record whose severity is exactly UVM_FATAL or exactly UVM_ERROR, using field-anchored tests only.
awk '$2 == "UVM_ERROR" || $2 == "UVM_FATAL" { print $1, $3, $4 }' sim.log420 Data mismatch
500 Bus hungTwo exact field tests joined with ||, which Chapter 4.2 formalises. Note that a single loose pattern /UVM_(ERROR|FATAL)/ would be shorter and would also match record 4 — Chapter 4.4 shows exactly that, and it is the same trade-off this chapter is about.
Exercise 6 — Challenge: choose the degree per column
Write one command over sim.log that selects records whose severity is exactly UVM_ERROR and whose message mentions mismatch, then explain why each half uses a different degree of tightness.
awk '$2 == "UVM_ERROR" && $0 ~ /mismatch/ { print $1, $3, $4 }' sim.log420 Data mismatchThe severity uses == because it is a closed vocabulary — the simulator emits one of a fixed set of severity words, so demanding the whole field is both correct and the tightest available test.
The message uses a loose match because it is free text. It has no fixed column — mismatch is in $4 here and could be in $5 or $7 on another record — so there is no position to anchor to, and contains is genuinely the question.
Writing $0 ~ /mismatch/ rather than bare /mismatch/ is a style choice: both test the whole record, and the explicit form makes it obvious to the next reader that the looseness is deliberate rather than an oversight. That is the habit this chapter is really arguing for — not avoiding loose matches, but making the choice visible.
12. Summary
- A pattern between slashes tests the whole record. It asks whether the text appears anywhere.
$n ~ /pattern/tests one field. It asks whether that column contains the text.- They usually agree, and the disagreement is silent. A path named
u_VIOLATEDwith a verdict ofMETis selected by the loose form and rejected by the field form. - This is the third appearance of one bug. A UVM_INFO message mentioning UVM_ERROR, a count built on it, and now a value in the wrong column — every time, the pattern asked whether and the engineer meant where.
- Three degrees of tightness: whole-record contains, field contains, field equals. Use
==for closed vocabularies like verdicts and severities,~for part of a field, and a loose match for free-form text with no column structure. !~selects records where a field does not match.- A variable can hold a pattern, with no slashes around it — and a dynamic pattern needs its backslashes doubled, so keep them to plain words for now.
- Matching is case-sensitive and POSIX awk has no switch for it. Check what the report actually says.
- Make the looseness visible.
$0 ~ /text/says "deliberately the whole record" where bare/text/might be an oversight.
You can now say exactly where a pattern is looking. What you cannot yet do is combine several tests into one rule with any precision — Exercise 5 used || and Exercise 6 used && on trust, and section 9 did the same.
Those operators have a precedence that genuinely changes which records you select, and getting it wrong produces a plausible wrong answer rather than an error. Chapter 4.2 — Comparison and Logical Operators is where the full set is laid out, including the one pair of parentheses that changes a result from three records to two.
Where this fits
Part of the awk curriculum.
