Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 33 · Debugging Failed Jobs and Broken Flows

Tracing What a Flow Did

A flow's output is what it chose to print; its trace is what it actually ran. This chapter makes the trace useful — naming the file and line and the call chain — then measures the two things nobody expects: that tracing a driver traces none of its children, and that switching tracing on reclassified a passing run as vacuous because the trace landed inside the artifact the verdict is read from.

Advanced26 min readLinuxPart IIDebuggingTracingset -x

Part II · Module 33 · Chapter 33.6 · Tracing What a Flow Did

Chapter 33.5 ended on the one piece of direct evidence about what a tool was asked to do. Here it is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ bash -x ./bin/regress 2>&1 | tee trace.log

A flow's output is a story it tells about itself. Its trace is a record of what it did. The two differ exactly where the bug is.

1. What the Trace Actually Shows

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ bash -x /tmp/t1.sh
+ d=/tmp/trace-demo
+ mkdir -p /tmp/trace-demo
++ ls /tmp/trace-demo
++ grep -c .
+ n=0
+ printf 'count=%s
' 0
count=0

Four properties of that output, and three of them are not obvious.

Every line is post-expansion. mkdir -p /tmp/trace-demo, not mkdir -p "$d". The trace is the argv, after parameter expansion, command substitution, word splitting, globbing and quote removal — which is why Chapter 33.5 called it the only direct evidence.

The depth marker counts nesting. ++ is one level in — a command substitution — and +++ is two. So ++ ls and ++ grep -c . are the two halves of $(ls "$d" | grep -c .), and the + n=0 below them is the assignment that consumed their result.

Assignments are traced. + n=0 is how you learn what a variable became without adding an echo.

And it quotes what needs quoting. From Chapter 33.3's harness:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
+ vvp '/tmp/m33demo/sim builds/sim.vvp' +TESTNAME=smoke +SEED=1

The single quotes are the shell telling you that argument is one argument. Compare the broken version, where the same path appears unquoted and is therefore two:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
+ vvp /tmp/m33demo/sim builds/sim.vvp +TESTNAME=smoke +SEED=

That one character is the entire diagnosis of a whole class of bug, and it is visible nowhere else.

2. Making It Say Where It Is

The default prefix is almost useless on a flow of any size:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /bin/bash -c 'printf "%q" "$PS4"'
+```

A plus and a space. On three hundred trace lines from four scripts, nothing says which file or which line. `PS4` fixes that, and it is re-evaluated on every traced line, so it can contain expansions:

```bash
$ PS4='+ ${BASH_SOURCE##*/}:${LINENO}: ' bash -x /tmp/t1.sh
+ t1.sh:2: d=/tmp/trace-demo
+ t1.sh:3: mkdir -p /tmp/trace-demo
++ t1.sh:4: ls /tmp/trace-demo
++ t1.sh:4: grep -c .
+ t1.sh:4: n=0
+ t1.sh:5: printf 'count=%s
' 0

Now every line is a coordinate. Add the function and you get the call chain for free:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' bash -x /tmp/t2.sh
+ t2.sh:4:main: outer hello
+ t2.sh:3:outer: inner hello
+ t2.sh:2:inner: echo 'in inner with hello'
+ in inner with hello

Three lines, three stack frames, with the argument visible at each hand-off. ${FUNCNAME[0]:-main} because FUNCNAME is empty at the top level, and the :-main is what stops the prefix collapsing there.

The ${BASH_SOURCE##*/} strips the directory — Chapter 8.3's suffix removal — because the full path of every script on every line is what makes a trace unreadable.

3. It Does Not Trace the Children

This is the finding that makes most flow traces useless, and it is invisible unless you look for it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ bash -x /tmp/parent.sh
+ echo 'parent: about to call the child'
parent: about to call the child
+ /tmp/child.sh
child: doing work

+ /tmp/child.sh and then nothing. The child ran, printed its output, and not one of its commands was traced. set -x is a property of a shell, and a child script is a different shell.

On the real driver that is the difference between a trace and a trace of the wrong thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the driver traced WITHOUT propagation ===
  lines=90   vvp invocations visible: 0

Ninety trace lines about setup, expansion and dispatch — and zero simulations, because the simulations happen inside one.sh, which xargs invokes as a separate script. The work the flow exists to do is the one part the trace omits.

The mechanism that does propagate

The documented answer is SHELLOPTS, and it does not work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /bin/bash -c 'SHELLOPTS=xtrace; echo "set to: $SHELLOPTS"'
/bin/bash: SHELLOPTS: readonly variable

Read-only. It reports the current options faithfully —

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /bin/bash -c 'set -x; echo "SHELLOPTS now: $SHELLOPTS"' 2>&1 | grep SHELLOPTS
+ echo 'SHELLOPTS now: braceexpand:hashall:interactive-comments:xtrace'

— and cannot be assigned, so it cannot be exported into a child.

What does work is BASH_ENV, which names a file every non-interactive bash sources at startup:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf "set -x\nPS4='+ \${BASH_SOURCE##*/}:\${LINENO}: '\n" > /tmp/xtrace-all.sh
$ BASH_ENV=/tmp/xtrace-all.sh bash -x ./bin/regress
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and WITH BASH_ENV propagation ===
  lines=381   vvp invocations visible: 2

And the children appear:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
+ regress:73: xargs -P 4 '-I{}' <R>/regr/20261009T110752Z/one.sh '{}'
+ one.sh:3: set -u
+ one.sh:3: set -u
+ one.sh:4: spec=prio0:1
+ one.sh:4: spec=smoke:1

Two one.sh:3 lines interleaved, because four slots are running — which is the next problem and the reason Chapter 31.6 isolates runs. A trace of a parallel flow is several traces shuffled together, and the only reliable way to read one is to re-run the failing spec alone.

4. The Volume, and What To Do About It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  plain                              12 lines
  bash -x (driver only)              90 lines
  bash -x + BASH_ENV (everything)    381 lines

32× at full propagation, and 78 of the 90 driver-only lines are trace rather than output. On a real nightly that factor applied to a four-hour flow is a file nobody opens.

Three ways to make it usable, in order of how often they are the right one:

Trace one spec, not the regression. Chapter 33.2's replay reduces the problem to one run before tracing enters the picture at all. A trace of one failing simulation is forty lines.

Turn tracing on for a region. set -x and set +x bracket the part you suspect:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set -x
"$root/bin/runsim" "$t" "$s" "$d" "$limit"
set +x

And grep the trace for the command you care about, which works because every traced line is a complete argv:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ grep -E '^\++ .*: (iverilog|vvp) ' trace.log

The ^\++ anchors on the depth marker so a line of program output that happens to mention vvp is not matched — the kind of over-match Chapter 24.1 measured in a signal search.

5. Tracing Changed the Result

Here is why this chapter is not simply "turn on set -x".

runsim runs the simulator in a redirected subshell, which is the right structure for Chapter 31.6's isolation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( cd "$d" && exec vvp "$root/logs/sim.vvp" … ) > "$d/run.log" 2>&1

With tracing on, the subshell's trace is part of the subshell's standard error — and that is redirected into run.log:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
+ runsim:12: cd <R>/regr/20261009T110752Z/runs/smoke_s1
+ runsim:12: exec vvp <R>/logs/sim.vvp +TESTNAME=smoke +SEED=1 +CYCLES=200
TB  test=smoke seed=1 cycles=200 verbosity=LOW
TB  TEST PASSED  test=smoke checks=3 errors=0
tb/tb_arb.v:165: $finish called at 85 (1s)

Two of the five lines in that log were written by the shell, not the simulator. For debugging this is wonderful — the second line is the exact argv, which is precisely what Chapter 33.1 needed. For the flow it is contamination of the artifact that every verdict is computed from.

On this regression the verdicts happened to survive — 1 PASS 1 FAIL traced and untraced. That is luck, and it is measurable luck:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat cap/tr/run.log
TB  test=smoke seed=1 cycles=200 verbosity=LOW
TB  TEST PASSED  test=smoke checks=3 errors=0
+ runsim:14: grep -q 'checks=0' run.log
+ runsim:15: echo 'ERROR none'

A log whose trace lines mention checks=0 — because the wrapper greps for it, so the trace quotes it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  checks extracted by  sed ... | tail -1  : 0
  checks the testbench actually reported  : 3

Chapter 32.3's extraction takes the last checks= in the log, and the last one is now a trace line. Running that chapter's classifier on it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ verdict cap/tr
VACUOUS
  exit=7

A passing run, reclassified as vacuous, by switching on tracing. Nothing failed; no error appeared; the regression simply reported a different result because it was being observed.

Practice

  1. Trace a short script and identify a post-expansion argv, a nesting marker and an assignment.
  2. Find a traced line with quotes in it and say what the quotes mean.
  3. Print the default PS4 with printf %q.
  4. Add the source file and line number and re-read a trace.
  5. Add the function name and trace a three-deep call chain.
  6. Explain ${FUNCNAME[0]:-main}.
  7. Put $(date +%T) in PS4 and count the forks it costs on a real flow.
  8. Use $SECONDS instead.
  9. Put $? in PS4 and work out which command's status it reports.
  10. Trace a script that calls another script and count the child's traced lines.
  11. Try to export SHELLOPTS.
  12. Use BASH_ENV and confirm the child is traced.
  13. Trace a parallel flow and find two interleaved children.
  14. Try BASH_XTRACEFD on your machine and check the file size.
  15. Check your bash version against 4.1.
  16. Measure the line-count ratio for plain, traced, and fully propagated.
  17. Grep a trace for one tool's invocations, anchored on the depth marker.
  18. Find a flow that redirects a subshell into an artifact.
  19. Trace it and look inside the artifact.
  20. Construct a trace line that a verdict rule picks up, and run the classifier.
  21. Work out which of your own log rules are loose enough to be fooled.
  22. Try all three fixes and count the leftover trace lines.
  23. Split stdout and stderr and demonstrate the lost interleaving.

Check Your Understanding

  1. What is the difference between a flow's output and its trace?
  2. Name the four properties of a traced line.
  3. What does ++ mean?
  4. What do quotes in a traced line tell you?
  5. What is the default PS4?
  6. Why can PS4 contain expansions?
  7. What does ${FUNCNAME[0]:-main} give at the top level, and why the default?
  8. What does a timestamp in PS4 cost, and what is free instead?
  9. Why is $? in PS4 misleading?
  10. What happens when a traced script calls another script?
  11. How many simulations were visible in the driver-only trace, and why?
  12. What is wrong with SHELLOPTS?
  13. What does BASH_ENV do?
  14. What appears in the trace once propagation is on?
  15. Why is a parallel flow's trace hard to read, and what is the fix?
  16. What does BASH_XTRACEFD do on this machine, and why?
  17. Why is that failure especially dangerous?
  18. Give the three line counts and the ratio.
  19. Give the three ways to reduce trace volume.
  20. Why anchor a trace grep on ^\++?
  21. Why did trace lines end up inside run.log?
  22. What did the trace line containing checks=0 do to the extraction?
  23. What did the classifier then report for a passing run?
  24. Which rule survived, and why?
  25. What does that argue for?
  26. Give the three fixes and their costs.
  27. What is the honest recommendation, and what does it generalise to?

6. Remember

  • Output is what a flow chose to print; the trace is what it ran.
  • Every traced line is the argv after all expansion, with quotes marking a single argument — the only direct evidence for a word-splitting bug.
  • ++ is one nesting level. Assignments are traced too.
  • Set PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' — and use $SECONDS, not $(date), which forks per line. Never $?: PS4 expands before the command runs.
  • bash -x does not trace child scripts. The driver's trace showed zero of its simulations.
  • SHELLOPTS is read-only. BASH_ENV pointing at a file containing set -x is what propagates.
  • BASH_XTRACEFD does nothing on bash 3.2 — the file stays empty and there is no error. It arrived in 4.1.
  • Volume: 12 → 90 → 381 lines. Trace one spec, bracket a region, and grep anchored on ^\++.
  • Tracing reclassified a passing run as VACUOUS, because the subshell's trace landed in run.log and the last checks= in the file was a trace line.
  • The tightly anchored rule survived and the loose one did not. Anchor patterns; never take the last match of a loose one.
  • No fix is free on bash 3.2: 1 leftover line, lost interleaving, or the tool's stderr off the log.
  • Trace a reproduction, not a regression — and remember that anything which adds output to a flow's artifacts changes the flow's results, verbosity included.

7. What Is Next

Tracing works on one run. Chapter 33.7 goes back to the other end of the scale — two hundred failures at once — and groups them by signature, so that one fix can be matched to the forty tests it actually explains rather than to the one somebody happened to open.

Where this fits

Part of the Linux & Bash curriculum.