Skip to content
VLSI Mentor

awk Part III · Module 10 · Timing and STA Report Analysis

Parsing Multi-Line EDA Reports

Every chapter in this module carried state across records, and the obvious objection is that awk already has a feature for multi-line records: set RS to the empty string and each blank-line-separated block becomes one record, fields and all. This chapter takes that objection seriously and measures it. On a compact report paragraph mode is the better tool and the shorter program. On the same report with its delay tables left in — which is what a timing tool actually emits — the identical program extracts nothing at all, because one blank line inside each record is enough to halve it.

Advanced19 min readawkPart IIISTAParagraph ModeRS

Part III · Module 10 · Chapter 10.7 · Parsing Multi-Line EDA Reports

1. Why This Matters

Chapters 10.2 through 10.6 all have the same skeleton: remember something from a header line, use it when the slack line arrives, clear it. That skeleton exists because a timing path spans many lines and awk processes one line at a time.

awk has a feature that appears to remove the need for it. Setting RS to the empty string puts awk in paragraph mode, where a record is a block of text delimited by blank lines rather than a single line. A whole timing path in one record, every field addressable, no carried state, no clearing, no mispairing.

It is the right answer for some report formats and it fails completely on others, and the line between the two is thinner than it looks. This chapter closes the module by measuring where that line is.

2. Paragraph Mode

Azvya Education Pvt. Ltd.VLSI Mentor
two.txt — synthetic teaching data. Two lines, each with a colon.
alpha:beta
gamma:delta
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = ""; FS = ":" } { printf "NF=%d:", NF; for (i=1;i<=NF;i++) printf " [%s]", $i; print "" }' two.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NF=4: [alpha] [beta] [gamma] [delta]

Two lines became one record with four fields. The colon split each line, and — this is the part that surprises people — the newline also acted as a field separator, in addition to FS. That behaviour is specific to paragraph mode and it is what makes the whole record addressable by field number.

The most useful variant sets FS to a newline, which makes each field a whole line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = ""; FS = "\n" } { printf "NF=%d  $1=[%s]  $NF=[%s]\n", NF, $1, $NF }' two.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NF=2  $1=[alpha:beta]  $NF=[gamma:delta]

NF is now the record's line count and $1, $2, $NF address lines. For a report whose records are fixed-shape blocks, that is a very direct way to work.

3. RS = "" Is Not RS = "\n\n"

Before using either, the difference is worth measuring, because the literal form looks equivalent and is not.

Azvya Education Pvt. Ltd.VLSI Mentor
gaps.txt — synthetic teaching data. Two blocks separated by three blank lines.
a 1
b 2



c 3
d 4
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'RS=""      : '; awk 'BEGIN { RS = "" } { printf "[rec %d: %d field(s)]", NR, NF } END { print "" }' gaps.txt
printf 'RS="\\n\\n"  : '; awk 'BEGIN { RS = "\n\n" } { printf "[rec %d: %d field(s)]", NR, NF } END { print "" }' gaps.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RS=""      : [rec 1: 4 field(s)][rec 2: 4 field(s)]
RS="\n\n"  : [rec 1: 4 field(s)][rec 2: 0 field(s)][rec 3: 4 field(s)]

Paragraph mode collapses a run of blank lines into one separator. The literal two-newline form does not, and produces an empty record for the extra blank line.

Leading and trailing blank lines differ too:

The fixture has three blank lines before the first block, one between the two blocks, and two after — a shape a fence cannot carry reliably, so it is built with printf:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '\n\n\nfirst\n\nsecond\n\n\n' > pad.txt
printf 'RS=""     : '; awk 'BEGIN { RS = "" } { printf "[%s]", $0 } END { printf " -> %d record(s)\n", NR }' pad.txt
printf 'RS="\\n\\n" : '; awk 'BEGIN { RS = "\n\n" } { printf "[%s]", $0 } END { printf " -> %d record(s)\n", NR }' pad.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RS=""     : [first][second] -> 2 record(s)
RS="\n\n" : [][
first][second][
] -> 4 record(s)

Two clean records against four, two of which are junk. And the field splitting differs as well:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "\n\n"; FS = ":" } { printf "RS literal NF=%d:", NF; for (i=1;i<=NF;i++) printf " [%s]", $i; print "" }' two.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RS literal NF=3: [alpha] [beta
gamma] [delta
]

NF=3, and the second field spans a line break — beta and gamma in one field — because the literal RS does not make a newline a field separator. Compare that against the NF=4 from section 2 on the same input.

4. The Measurement

Two reports. hidden.rpt from Chapter 10.6 is compact — seven lines per path, no blank line inside a path. timing.rpt from Chapter 10.1 is what a timing tool emits — the same headers plus the delay table, with a blank line between the header block and the table.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf "full report (delay tables):   "; awk 'BEGIN { RS = "" } { n++ } /Startpoint/ && /slack/ { b++ } END { printf "%d paragraph(s), %d containing BOTH a Startpoint and a slack\n", n, b+0 }' timing.rpt
printf "compact report (no tables):   "; awk 'BEGIN { RS = "" } { n++ } /Startpoint/ && /slack/ { b++ } END { printf "%d paragraph(s), %d containing BOTH a Startpoint and a slack\n", n, b+0 }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
full report (delay tables):   7 paragraph(s), 0 containing BOTH a Startpoint and a slack
compact report (no tables):   3 paragraph(s), 3 containing BOTH a Startpoint and a slack

Zero against three. On the full report not one paragraph contains both a path's identity and its answer, because the blank line before the delay table splits every path into two paragraphs: a header block and a table block, and the slack line is at the end of the table.

Now one program against both inputs:

Azvya Education Pvt. Ltd.VLSI Mentor
para.awk — synthetic teaching script. Paragraph mode.
BEGIN { RS = "" }
{
    s = ""; sl = ""
    for (i = 1; i < NF; i++) {
        if ($i == "Startpoint:") s = $(i + 1)
        if ($i == "slack") sl = $NF
    }
    printf "para %d: %-22s %8s\n", NR, (s == "" ? "<none>" : s), (sl == "" ? "<none>" : sl)
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f para.awk hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
para 1: u_regfile/dout_reg[3]    -0.018
para 2: u_periph/req_reg[0]      -0.205
para 3: u_io/pad_reg[0]          -0.023

Three paths, correct. The field scan is the paragraph-mode idiom: walk the fields looking for a marker and take the one after it — which works because the newline-as-field-separator rule put every word of the record into the field list.

The same program on the full report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f para.awk timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
para 1: <none>                   <none>
para 2: u_regfile/dout_reg[3]    <none>
para 3: <none>                   -0.018
para 4: u_ctrl/state_reg[1]      <none>
para 5: <none>                    0.067
para 6: u_io/pad_reg[0]          <none>
para 7: <none>                   -0.023

Seven rows and not one of them is a path. The names and the numbers are all present and correct, in alternating records, and nothing pairs them. Every value the program needs is in the file; the record boundary is in the wrong place.

And the state-carrying form from Chapter 10.2:

Azvya Education Pvt. Ltd.VLSI Mentor
state.awk — synthetic teaching script. State-carrying.
/^ *Startpoint:/ { s = $2 }
/^ *slack/ { printf "%-22s %8s\n", (s == "" ? "<none>" : s), $NF; s = "" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f state.awk hidden.rpt
awk -f state.awk timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_regfile/dout_reg[3]    -0.018
u_periph/req_reg[0]      -0.205
u_io/pad_reg[0]          -0.023
u_regfile/dout_reg[3]    -0.018
u_ctrl/state_reg[1]       0.067
u_io/pad_reg[0]          -0.023

Three correct rows from each. Two lines of awk, and the blank lines are irrelevant to it because it never used them for anything.

5. Choosing

Six-step procedure for choosing between paragraph mode and state-carrying when parsing a multi-line report: count the records, check whether any record contains a blank line, confirm every record carries both ends, choose the mode, verify the record count against a marker count, and state the choice in the output.01COUNT THE RECORDSRun RS equals empty on the real file and compare the paragraph countagainst the number of blocks you expect.02LOOK FOR INNER BLANKSA blank line inside a record halves it. This is the measurement thatdecides the rest.03CHECK BOTH ENDS LANDCount the records holding a start marker and an end marker. Zero meansthe boundary is wrong.04CHOOSE THE MODEA few fields from a fragile shape favours carried state; a conditionover the whole block favours paragraph mode.05VERIFY AGAINST MARKERSCompare the record count against the marker counts, as every chapter inthis module does.06STATE THE CHOICEPrint the record count and the mode, so a report that silently changesshape is visible in the output.
Figure 1 — Deciding how to read a multi-line report. The first three steps are measurements on the actual file rather than assumptions about the format; only then is the choice between paragraph mode and carried state a decision rather than a guess.

Step 3 is the cheap one-liner worth running before anything else:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for f in hidden.rpt timing.rpt; do printf "%-12s " "$f"; awk 'BEGIN { RS = "" } { n++ } /Startpoint/ && /slack/ { b++ } END { printf "%d record(s), %d with both ends -> %s\n", n, b+0, (b == n ? "paragraph mode is safe" : "PARAGRAPH MODE WILL NOT WORK") }' $f; done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hidden.rpt   3 record(s), 3 with both ends -> paragraph mode is safe
timing.rpt   7 record(s), 0 with both ends -> PARAGRAPH MODE WILL NOT WORK

Three records and three usable against seven records and none usable. The verdict needs no judgement: if the number of records carrying both ends is not the number of records, the boundary is in the wrong place and the mode cannot be used. Running that one line against a real report costs nothing and replaces an assumption about the format with a measurement of the file.

6. Where Paragraph Mode Wins

The case for it is not brevity in general — state.awk is two lines. It is a condition over the whole record:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = ""; FS = "\n" } /clk_periph/ && /clk_ddr/ { print "---- cross-domain block ----"; print $0 }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
---- cross-domain block ----
  Startpoint: u_periph/req_reg[0]
              (rising edge-triggered flip-flop clocked by clk_periph)
  Endpoint: u_mem/wdata_reg[0]
            (rising edge-triggered flip-flop clocked by clk_ddr)
  Path Group: clk_ddr
  Path Type: max
  slack (VIOLATED)                                   -0.205

One pattern — two regexes joined by && — finds the block that mentions both clocks and prints it whole. Chapter 10.6 needed a stage variable, two carried clock names and a positional parse to reach the same conclusion.

Counting how many clocks each block mentions is a single rule:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "" } { n = 0; if (/clk_core/) n++; if (/clk_ddr/) n++; if (/clk_io/) n++; if (/clk_periph/) n++; printf "block %d mentions %d clock(s)\n", NR, n }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
block 1 mentions 1 clock(s)
block 2 mentions 2 clock(s)
block 3 mentions 1 clock(s)

The line-oriented equivalent has to accumulate the block by hand:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^ *Startpoint:/ { blk = $0; next } /^ *slack/ { blk = blk "\n" $0; nc = 0; if (blk ~ /clk_core/) nc++; if (blk ~ /clk_ddr/) nc++; if (blk ~ /clk_io/) nc++; if (blk ~ /clk_periph/) nc++; printf "block %d mentions %d clock(s)\n", ++n, nc; blk = ""; next } { blk = blk "\n" $0 }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
block 1 mentions 1 clock(s)
block 2 mentions 2 clock(s)
block 3 mentions 1 clock(s)

Same answer, three rules instead of one, with a hand-built accumulator and two next statements to keep the header and slack lines from being appended twice.

And FS = "\n" makes a fixed-shape block addressable by line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = ""; FS = "\n" } { printf "block %d: %d line(s); line 1 = [%s]; last = [%s]\n", NR, NF, $1, $NF }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
block 1: 7 line(s); line 1 = [  Startpoint: u_regfile/dout_reg[3]]; last = [  slack (VIOLATED)                                   -0.018]
block 2: 7 line(s); line 1 = [  Startpoint: u_periph/req_reg[0]]; last = [  slack (VIOLATED)                                   -0.205]
block 3: 7 line(s); line 1 = [  Startpoint: u_io/pad_reg[0]]; last = [  slack (VIOLATED)                                   -0.023]

$1 is the startpoint line and $NF is the slack line, addressed directly, with no state and no pattern matching — which is exactly what paragraph mode is for, on a file whose blocks are reliable.

7. Common Mistakes

Assuming a blank-line record boundary is part of the format. It is usually presentational — section 4.

Using RS = "\n\n" as an explicit paragraph mode. It yields empty records, keeps leading and trailing blanks, and does not split fields on newlines — section 3.

Not counting the records before trusting them. Paragraph mode fails silently with well-formed output — section 5.

Expecting FS alone to split a paragraph-mode record. A newline splits too, which is the behaviour that makes the field scan work.

Forgetting that FS = "\n" makes NF a line count. Useful, and easy to misread as a field count.

Mixing modes in one program. RS is global; a file with a line-oriented preamble needs two passes.

Reaching for paragraph mode to avoid carried state. State-carrying was two lines and worked on both shapes — section 4.

Dismissing paragraph mode for everything. A condition over the whole record is one pattern instead of three rules — section 6.

8. Exercises

Exercise 1 — Count the paragraphs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "" } END { printf "%d paragraph(s)\n", NR }' hidden.rpt
awk 'BEGIN { RS = "" } END { printf "%d paragraph(s)\n", NR }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 paragraph(s)
7 paragraph(s)

Three paths read as three records in one file and as seven in the other.

Exercise 2 — Address lines by number

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = ""; FS = "\n" } { printf "block %d: %d line(s), first [%s]\n", NR, NF, $1 }' hidden.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
block 1: 7 line(s), first [  Startpoint: u_regfile/dout_reg[3]]
block 2: 7 line(s), first [  Startpoint: u_periph/req_reg[0]]
block 3: 7 line(s), first [  Startpoint: u_io/pad_reg[0]]

Exercise 3 — Debug a parser that extracts nothing

A paragraph-mode extractor returns seven rows from a three-path report, each missing half its data.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f para.awk timing.rpt | head -3
awk 'BEGIN { RS = "" } /Startpoint/ && /slack/ { b++ } END { printf "%d record(s) contain both ends\n", b+0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
para 1: <none>                   <none>
para 2: u_regfile/dout_reg[3]    <none>
para 3: <none>                   -0.018
0 record(s) contain both ends

Seven records and none of them holds both a startpoint and a slack. Anchoring the patterns to the markers confirms it and shows where the halves went:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "" } /(^|\n) *Startpoint:/ { st++ } /(^|\n) *slack \(/ { sl++ } /(^|\n) *Startpoint:/ && /(^|\n) *slack \(/ { both++ } END { printf "records with a startpoint %d, with a slack %d, with BOTH %d\n", st+0, sl+0, both+0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
records with a startpoint 3, with a slack 3, with BOTH 0

Three records hold a startpoint, three hold a slack, and none holds both. The blank line before each delay table put the boundary between them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f state.awk timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_regfile/dout_reg[3]    -0.018
u_ctrl/state_reg[1]       0.067
u_io/pad_reg[0]          -0.023

Exercise 4 — Debug RS = "\n\n"

A parser using the literal two-newline separator reports more records than the file has blocks.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "\n\n" } { printf "[rec %d: %d field(s)]", NR, NF } END { print "" }' gaps.txt
awk 'BEGIN { RS = "" } { printf "[rec %d: %d field(s)]", NR, NF } END { print "" }' gaps.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[rec 1: 4 field(s)][rec 2: 0 field(s)][rec 3: 4 field(s)]
[rec 1: 4 field(s)][rec 2: 4 field(s)]

The extra blank lines produce a record with no fields. Paragraph mode collapses the run.

Exercise 5 — Engineering application

A reader that measures the file before choosing, and refuses rather than guessing.

Azvya Education Pvt. Ltd.VLSI Mentor
readmode.awk — synthetic teaching script.
# readmode.awk — report whether a file can be read in paragraph mode.
#
# Usage:  awk -f readmode.awk hidden.rpt timing.rpt
# Exit:   0 every file is safe for paragraph mode
#         1 at least one file is not

BEGIN { RS = "" }

FNR == 1 && NR > 1 { report() }
FNR == 1 { file = FILENAME; n = 0; st = 0; sl = 0; both = 0 }

{
    n++
    hs = ($0 ~ /(^|\n) *Startpoint:/)
    hl = ($0 ~ /(^|\n) *slack \(/)
    st += hs
    sl += hl
    both += (hs && hl)
}

END { report() ; exit (bad ? 1 : 0) }

function report() {
    printf "%-14s %2d record(s), %2d with a startpoint, %2d with a slack, %2d with BOTH  -> %s\n",
           file, n, st, sl, both,
           (both == n && n > 0 ? "paragraph mode" : "carried state")
    if (!(both == n && n > 0)) bad = 1
    n = 0; st = 0; sl = 0; both = 0
    file = FILENAME
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f readmode.awk hidden.rpt timing.rpt
echo "status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hidden.rpt      3 record(s),  3 with a startpoint,  3 with a slack,  3 with BOTH  -> paragraph mode
timing.rpt      7 record(s),  3 with a startpoint,  3 with a slack,  0 with BOTH  -> carried state
status: 1

Two files, two verdicts, from a measurement rather than an assumption about the format. The FNR == 1 && NR > 1 rule is the per-file flush from Chapter 7.4 section 7 — in paragraph mode FNR counts records rather than lines, so it still identifies the first record of each file.

Note the exit status is 1 because one file needs carried state. That is the useful polarity for a wrapper script: a non-zero status means do not use the paragraph-mode path for this set of inputs, which is a decision, not an error.

Exercise 6 — Challenge: show that the paragraph-mode failure passes a reasonable sanity check

A reviewer proposes validating the paragraph-mode parser by checking that it emits one row per record and that every slack value it prints appears in the report. Show that both checks pass on the broken run.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f para.awk timing.rpt | wc -l
awk 'BEGIN { RS = "" } END { print NR }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
       7
7

Seven rows for seven records — the first check passes exactly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f para.awk timing.rpt | awk '$NF != "<none>" { v++; while ((getline l < "timing.rpt") > 0) if (index(l, $NF)) { f++; break }; close("timing.rpt") } END { printf "%d slack value(s) printed, %d found in the report\n", v+0, f+0 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
3 slack value(s) printed, 3 found in the report

Every value the parser printed is genuinely in the report. The second check passes too.

Both checks pass, and the parser produced not one correct path. What neither check looks at is whether a row's two halves came from the same path — and they cannot, because each half is individually valid.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN { RS = "" } /(^|\n) *Startpoint:/ && /(^|\n) *slack \(/ { both++ } END { printf "records carrying both halves: %d\n", both+0 }' timing.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
records carrying both halves: 0

One number, measured on the input rather than the output, settles it. Zero records carry both halves, so no correct row was possible and the seven that appeared are all artefacts.

That is the module's closing lesson, and it has now been reached from five directions: Chapter 10.2's count of startpoints against slack lines, Chapter 10.3's per-row comparison of two methods, Chapter 10.4's Path Type count against the path count, Chapter 10.6's startpoint count against endpoint count, and this. A multi-line parser is validated against the structure of its input, never against the shape of its output — because the output's shape is determined by the program, and a wrong program produces a well-formed wrong answer.

9. Summary

  • RS = "" is paragraph mode: records are blank-line-delimited, runs of blank lines collapse, leading and trailing blanks are ignored, and a newline is a field separator in addition to FS.
  • RS = "\n\n" is none of those things. It yields empty records and fields that span lines — section 3.
  • FS = "\n" with paragraph mode makes NF a line count and $1, $NF address lines.
  • A blank line inside a record halves it. On the full timing report, zero of seven paragraphs carried both a path's identity and its slack — section 4.
  • One program, two inputs differing by a cosmetic blank line: 3 correct rows and 0. State-carrying gave 3 from both, in two lines of awk.
  • The failure is silent. Seven well-formed rows, exit 0, real registers, real slack values, no correct path.
  • Measure before choosing. Count the records and count how many carry both ends — section 5.
  • Paragraph mode wins on a condition over the whole record — one pattern against three rules with a hand-built accumulator — section 6.
  • Validate a multi-line parser against the structure of its input. A row count and a value lookup both passed on a run with no correct rows — Exercise 6.

That closes Module 10 and with it the hardest parsing problem in this track: a record that spans lines has no boundary awk can see, so every answer depends on something the program decided rather than something the file said. Every chapter here ended at the same place — a precondition counted on the input, before the parse is trusted.

Where this fits

Part of the awk curriculum.