Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 24 · Searching Design, Verification and Log Text

Turning a Search Into an Answer

Nobody ever asked for matching lines. They asked how many, which ones, or where — and the gap between grep output and an answer is three or four Part I stages. This chapter closes the module by crossing it, with the one sorting mistake that quietly ranks every number wrong.

Intermediate21 min readLinuxPart IIPipelinesawkReporting

Part II · Module 24 · Chapter 24.6 · Turning a Search Into an Answer

A lead asks "how many modules are in the AXI block", "which tests touch the scoreboard", or "where are the errors coming from". None of those is answered by a screen of matching lines, and pasting one into a message is how you get asked again.

Three shapes answer almost every question: a count, a list, a table.

Three shapes, and the stages that produce them

Three answer shapes and the pipeline stages that produce them. A count is search then tally. A list is search, extract, sort. A table is search, extract, group, align. Each shape answers a different question: how many, which ones, and where and how many at once.A countsearch, then tallyA listextract, then sortA tablegroup, then alignAnswers how manyrestate the question with itAnswers which onessorted, never deduplicatedblindlyAnswers where and howmanywith a header and a total12
Each shape is the previous one plus a stage. A count is a search and a tally; a list adds extraction so you can say which; a table adds grouping and alignment so you can say where and how many at once. Choosing the shape before writing the pipeline is what stops a search becoming a screen of matching lines nobody reads.

1. A Count

The whole answer is one number, and the pipeline is grep then wc:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rhE '^[[:space:]]*module[[:space:]]' --include='*.sv' rtl | wc -l
       5

Two details make this correct rather than nearly correct.

-h suppresses the file name, which matters because grep -rc gives a per-file count and summing it needs awk:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rhcE '^[[:space:]]*module[[:space:]]' --include='*.sv' rtl | awk '{s+=$1} END {print s}'
5

Both give 5. -c with awk is the one to use on a huge tree, because it never transfers the matching lines; -h | wc -l is clearer and fine up to millions of lines.

And wc -l right-pads on BSD, which is why the output above is 5. In a report that is ugly and in a comparison it is a bug:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
n=$(... | wc -l | tr -d ' ')          # or
n=$(... | wc -l); n=$(( n ))          # arithmetic strips it

Chapter 4.5 measured that padding. The second form is the tidier one: $(( )) parses the number and discards the whitespace.

A bare number is still not an answer. Restate the question with it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf 'modules in rtl/: %s\n' "$(/usr/bin/grep -rhE '^[[:space:]]*module[[:space:]]' --include='*.sv' rtl | wc -l | tr -d ' ')"
modules in rtl/: 5

Five words of context, and the recipient does not have to ask what was counted or where.

2. A List

A count says how many; a list says which. The extra stage is extraction, and sed with a capture group is the tool:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rhE '^[[:space:]]*module[[:space:]]+' --include='*.sv' rtl |
>   sed -E 's/^[[:space:]]*module[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*).*/\1/' | sort
axi_master
axi_slave
chip_top
clk_div
uart_tx

Five names, alphabetical, nothing else. Three decisions:

sed -E with \1, which replaces the whole line with the captured identifier. The trailing .* is essential — without it the rest of the line survives the substitution, because sed replaces only what matched. Chapter 13.6 §4 is the capture-group mechanism.

sort at the end, not sort -u — here the names are already unique, and using -u would silently hide a duplicate module name, which is a defect you want to see. When you do want deduplication, sort | uniq -c tells you how many duplicates, which sort -u throws away.

And grep before sed, not sed alone. sed could do both jobs, and grep is faster at rejecting lines — which is Chapter 13.9 §5's rule that the cheapest, most selective stage goes first.

3. A Table

A table answers "where" and "how many" at once, and it is the shape most questions actually want.

Counted by directory:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rlE '^[[:space:]]*module[[:space:]]' --include='*.sv' rtl |
>   sed 's|/[^/]*$||' | sort | uniq -c | sort -rn
   2 rtl/core
   2 rtl/axi
   1 rtl/uart

grep -l for file names, sed 's|/[^/]*$||' to strip the last component — dirname for a stream, with no process per line — then the sort | uniq -c | sort -rn idiom from Chapter 13.4.

Or as a lookup, which is the most useful table in an unfamiliar repository:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -rnE '^[[:space:]]*module[[:space:]]+' --include='*.sv' rtl |
>   sed -E 's|^([^:]*):([0-9]*):[[:space:]]*module[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*).*|\3\t\1:\2|' |
>   sort | awk -F'\t' 'BEGIN { printf "%-14s %s\n", "MODULE", "DEFINED AT" }
>                            { printf "%-14s %s\n", $1, $2 }'
MODULE         DEFINED AT
axi_master     rtl/axi/axi_master.sv:2
axi_slave      rtl/axi/axi_slave.sv:2
chip_top       rtl/core/chip_top.sv:2
clk_div        rtl/core/clk_div.sv:1
uart_tx        rtl/uart/uart_tx.sv:1

Five modules, where each is defined, with a header. Four things are doing work:

grep -n to keep the line number, so the table's second column is a location you can open rather than just a file.

The sed reorders three captures into two tab-separated fields. Reordering is why this is sed and not -o.

A tab as the field separator and awk -F'\t', because a path contains no tab while it may contain a space. Choosing a separator the data cannot contain is the whole trick — and it is why the log tables in §4 use |.

And awk doing the alignment, with a BEGIN block for the header. Chapter 13.7 is awk; printf with %-14s is what turns columns into a table rather than a list with spaces in it.

4. A Table From a Log

The same three shapes, applied to Chapter 24.4's log. Every warning and error, in time order, as columns:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -E '^UVM_(ERROR|WARNING) [^ ]*\([0-9]+\) @ ' run_2026_10_09/axi_burst_test.log |
>   sed -E 's/^(UVM_[A-Z]+) ([^ ]+) @ ([0-9]+): ([^ ]+) \[([A-Z_]+)\].*/\1|\3|\4|\5|\2/' |
>   sort -t'|' -k2,2n |
>   awk -F'|' 'BEGIN { printf "%-12s %8s  %-26s %-10s %s\n", "SEVERITY","TIME","SCOPE","ID","SOURCE" }
>                    { printf "%-12s %8s  %-26s %-10s %s\n", $1,$2,$3,$4,$5 }'
SEVERITY         TIME  SCOPE                      ID         SOURCE
UVM_WARNING      2500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_WARNING      7500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_WARNING     12500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_ERROR       13370  uvm_test_top.env.sb        MISMATCH   tb/env/axi_sb.sv(201)
UVM_WARNING     17500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_WARNING     22500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_WARNING     27500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_ERROR       29000  uvm_test_top.env.sb        MISMATCH   tb/env/axi_sb.sv(201)
UVM_WARNING     32500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)
UVM_WARNING     37500  uvm_test_top.env.sb        SB_LATE    tb/env/axi_sb.sv(88)

Ten rows out of 4021 lines, and the shape of the failure is now visible: the warnings are regular — every 5000 time units — and the two errors are not. A regular warning is a configuration artefact; an irregular error is a bug. You cannot see that in a list of matching lines and you can see it immediately in a time-sorted table.

5. A Census With a Total

The last shape, and the one a lead actually reads:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -oE '\[[A-Z_]+\]' run_2026_10_09/axi_burst_test.log |
>   sort | uniq -c | sort -rn |
>   awk 'BEGIN { printf "%8s  %s\n", "COUNT", "MESSAGE ID" }
>              { printf "%8s  %s\n", $1, $2; t += $1 }
>        END   { printf "%8s  %s\n", t, "(total)" }'
   COUNT  MESSAGE ID
    4000  [AXI_MON]
       8  [SB_LATE]
       2  [MISMATCH]
       1  [TEST_DONE]
       1  [RNTST]
       1  [BUILD]
    4013  (total)

The total row is the point. 4013 of 4021 lines are accounted for, so eight lines have no message id — the banner, the summary and the verdict. A census whose total does not reconcile with the file is a census that is quietly dropping something, and printing the total is how you find out.

That reconciliation is worth doing deliberately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /usr/bin/grep -c '' run_2026_10_09/axi_burst_test.log
4021

4021 lines, 4013 categorised, 8 unaccounted for. Being able to say where every line went is the difference between a summary and a guess — and it is the same discipline as Chapter 24.4 §2's three agreeing counts.

6. The One-Line Row

A regression produces hundreds of logs, and each needs to become one line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf '%-22s %-6s %4s %4s %4s\n' 'axi_burst_test' \
>   "$(/usr/bin/grep -qE '^TEST PASSED' "$L" && echo PASS || echo FAIL)" \
>   "$(/usr/bin/grep -cE '^UVM_ERROR [^ ]*\([0-9]+\) @ ' "$L")" \
>   "$(/usr/bin/grep -cE '^UVM_FATAL [^ ]*\([0-9]+\) @ ' "$L")" \
>   "$(/usr/bin/grep -cE '^UVM_WARNING [^ ]*\([0-9]+\) @ ' "$L")"
axi_burst_test         FAIL      2    0    8

One row per test, fixed-width, so a hundred of them is a table without any further work. Two notes:

grep -q for the verdict, which exits on the first match and prints nothing — the status is the answer. Chapter 16.7's command-as-condition.

And four separate grep passes over the same file, which is wasteful and correct at this size. At regression scale it becomes one awk pass:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '
  /^TEST PASSED/                              { v = "PASS" }
  /^TEST FAILED/                              { v = "FAIL" }
  /^UVM_ERROR [^ ]*\([0-9]+\) @ /             { e++ }
  /^UVM_FATAL [^ ]*\([0-9]+\) @ /             { f++ }
  /^UVM_WARNING [^ ]*\([0-9]+\) @ /           { w++ }
  END { printf "%-22s %-6s %4d %4d %4d\n", name, (v?v:"NO-VERDICT"), e, f, w }
' name="$(basename "${L%.log}")" "$L"

One traversal, five questions — and (v?v:"NO-VERDICT") keeps §24.4's third verdict state. Reading a multi-gigabyte log once instead of five times is the difference between a regression summary that takes a minute and one that takes five. Chapter 13.7 §6 made the general argument; this is where it pays.

7. Choosing the Shape

The questionThe shapeThe pipeline
"How many?"a count, with the question restatedgrep -c → awk sum, or grep -h → wc -l
"Which ones?"a sorted listgrep → sed capture → sort
"Where, and how many?"a counted tablegrep -l → sed → sort | uniq -c | sort -rn
"What happened, in order?"a column tablegrep → sed to fields → sort -k2,2n → awk
"What is this made of?"a census with a totalgrep -o → sort | uniq -c → awk with END
"Pass or fail?"one fixed-width rowone awk pass

And the rule that makes all six readable: put the question in the output. A header row, a restated count, a labelled total. The recipient did not run your command and cannot reconstruct what the columns are.

Practice

  1. Count the modules in a repository you use. Then restate the count as a sentence.
  2. Do it both ways — -h | wc -l and -c | awk — and confirm they agree.
  3. Strip wc -l's padding two ways and check both.
  4. Produce the sorted module list. Use sort rather than sort -u, then find out whether any name is duplicated.
  5. Build the module-to-file table. Open one of the locations it reports.
  6. Change the tab separator to a space and find a path that breaks the awk.
  7. Build a time-ordered table from a log you have. Is the irregular event visible?
  8. Drop the n from the sort key and compare the two orders. How obvious is the wrong one?
  9. Change -k2,2 to -k2 and construct two rows where it matters.
  10. Build a census with a total. Does the total reconcile with grep -c ''?
  11. Find the lines your census does not categorise. What are they?
  12. Write the one-line row for a log, then the single-pass awk version, and confirm they agree.
  13. Time both on the largest log you have.

Check Your Understanding

  1. What are the three shapes that answer most questions?
  2. Why does grep -h matter when counting?
  3. Why is a bare number not an answer?
  4. Why sort rather than sort -u for the module list?
  5. Why is the trailing .* essential in the sed capture?
  6. When does -o beat sed, and when does it not?
  7. Why choose a tab or | as a field separator?
  8. What does sort -t'|' -k2,2 do wrong, and why is it hard to spot?
  9. What is the difference between -k2 and -k2,2?
  10. What does the total row in a census tell you?
  11. Why replace four grep passes with one awk pass?

Chapter 29.6 is the case where there is nothing to search for: an uninitialised value poisoned every expression it touched and the simulator issued no message at all.

8. Remember

  • Three shapes: a count, a list, a table. Nobody asked for matching lines.
  • grep -h when counting, or the file names come too; grep -rc | awk sums without transferring the lines.
  • BSD wc -l right-pads. Strip it with tr -d ' ' or $(( )).
  • Restate the question in the output. "modules in rtl/: 5", not "5".
  • sed -E with a capture and a trailing .* to extract; without the .* the rest of the line survives.
  • sort, not sort -u — a duplicate is a finding. sort | uniq -c keeps the count.
  • grep before sed: the cheapest, most selective stage goes first.
  • -o for a self-contained token, sed when you need part of a match or a reorder.
  • Pick a separator the data cannot contain — a tab for paths, | for log fields.
  • sort -t'|' -k2,2n: without n the field sorts as text and 100 precedes 20. -k2,2, not -k2.
  • awk with a BEGIN header and printf "%-14s" is what makes a table.
  • Print a total and reconcile it with grep -c ''. 4013 of 4021 means eight lines went somewhere.
  • One fixed-width row per test, and one awk pass rather than four greps, at regression scale.

9. Module 24 in One Page

  • 24.1 — ten hits for one signal, one of them the declaration. Narrow in four stages; -w's word characters are letters, digits and underscore only, so it fixes substrings and nothing else.
  • 24.2 — match the construct, not the string. #\([^)]*\) found two of three instantiations because a negated class cannot span nested parentheses.
  • 24.3 — --include='*.{sv,v,c,py}' finds nothing and exits 1, because quoting suppresses brace expansion. Repeated options or an array are the only forms that behave the same in bash, zsh and dash.
  • 24.4 — logs are the opposite problem. grep -c '^UVM_ERROR' reported 3 where the log said 2, and invented a UVM_FATAL that never happened, because the summary lines match the pattern.
  • 24.5 — half of what a design does is decided in build scripts. -DADDR_W=8 was accepted, exited 0, printed nothing, and had no effect.
  • 24.6 — turn it into a count, a list or a table, and put the question in the output.

Two themes ran through all six. Every chapter's headline failure was silent: a wrong count, a missing instance, an empty result, an invented fatal, an ignored flag. None of them errored, and each was found only by knowing what the answer should have been. And the fix was never a cleverer pattern — it was a second, independent way of getting the same number, which is the habit Chapter 23.3 §3 introduced and which this module has now used five more times.

Module 25 moves from reading the build description to running it: module systems, environment files, licence servers, and what source setup.sh actually did to your shell.

Where this fits

Part of the Linux & Bash curriculum.