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:
$ bash -x ./bin/regress 2>&1 | tee trace.logA 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
$ 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=0Four 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:
+ vvp '/tmp/m33demo/sim builds/sim.vvp' +TESTNAME=smoke +SEED=1The 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:
+ 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:
$ /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
' 0Now every line is a coordinate. Add the function and you get the call chain for free:
$ 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 helloThree 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.
$ 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:
=== the driver traced WITHOUT propagation ===
lines=90 vvp invocations visible: 0Ninety 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:
$ /bin/bash -c 'SHELLOPTS=xtrace; echo "set to: $SHELLOPTS"'
/bin/bash: SHELLOPTS: readonly variableRead-only. It reports the current options faithfully —
$ /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:
$ printf "set -x\nPS4='+ \${BASH_SOURCE##*/}:\${LINENO}: '\n" > /tmp/xtrace-all.sh
$ BASH_ENV=/tmp/xtrace-all.sh bash -x ./bin/regress=== and WITH BASH_ENV propagation ===
lines=381 vvp invocations visible: 2And the children appear:
+ 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:1Two 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
plain 12 lines
bash -x (driver only) 90 lines
bash -x + BASH_ENV (everything) 381 lines32× 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:
set -x
"$root/bin/runsim" "$t" "$s" "$d" "$limit"
set +xAnd grep the trace for the command you care about, which works because every traced line is a complete argv:
$ grep -E '^\++ .*: (iverilog|vvp) ' trace.logThe ^\++ 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:
( cd "$d" && exec vvp "$root/logs/sim.vvp" … ) > "$d/run.log" 2>&1With tracing on, the subshell's trace is part of the subshell's standard error — and that is redirected into run.log:
+ 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:
$ 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:
checks extracted by sed ... | tail -1 : 0
checks the testbench actually reported : 3Chapter 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:
$ verdict cap/tr
VACUOUS
exit=7A 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
- Trace a short script and identify a post-expansion argv, a nesting marker and an assignment.
- Find a traced line with quotes in it and say what the quotes mean.
- Print the default
PS4withprintf %q. - Add the source file and line number and re-read a trace.
- Add the function name and trace a three-deep call chain.
- Explain
${FUNCNAME[0]:-main}. - Put
$(date +%T)inPS4and count the forks it costs on a real flow. - Use
$SECONDSinstead. - Put
$?inPS4and work out which command's status it reports. - Trace a script that calls another script and count the child's traced lines.
- Try to export
SHELLOPTS. - Use
BASH_ENVand confirm the child is traced. - Trace a parallel flow and find two interleaved children.
- Try
BASH_XTRACEFDon your machine and check the file size. - Check your bash version against 4.1.
- Measure the line-count ratio for plain, traced, and fully propagated.
- Grep a trace for one tool's invocations, anchored on the depth marker.
- Find a flow that redirects a subshell into an artifact.
- Trace it and look inside the artifact.
- Construct a trace line that a verdict rule picks up, and run the classifier.
- Work out which of your own log rules are loose enough to be fooled.
- Try all three fixes and count the leftover trace lines.
- Split stdout and stderr and demonstrate the lost interleaving.
Check Your Understanding
- What is the difference between a flow's output and its trace?
- Name the four properties of a traced line.
- What does
++mean? - What do quotes in a traced line tell you?
- What is the default
PS4? - Why can
PS4contain expansions? - What does
${FUNCNAME[0]:-main}give at the top level, and why the default? - What does a timestamp in
PS4cost, and what is free instead? - Why is
$?inPS4misleading? - What happens when a traced script calls another script?
- How many simulations were visible in the driver-only trace, and why?
- What is wrong with
SHELLOPTS? - What does
BASH_ENVdo? - What appears in the trace once propagation is on?
- Why is a parallel flow's trace hard to read, and what is the fix?
- What does
BASH_XTRACEFDdo on this machine, and why? - Why is that failure especially dangerous?
- Give the three line counts and the ratio.
- Give the three ways to reduce trace volume.
- Why anchor a trace grep on
^\++? - Why did trace lines end up inside
run.log? - What did the trace line containing
checks=0do to the extraction? - What did the classifier then report for a passing run?
- Which rule survived, and why?
- What does that argue for?
- Give the three fixes and their costs.
- 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$?:PS4expands before the command runs. bash -xdoes not trace child scripts. The driver's trace showed zero of its simulations.SHELLOPTSis read-only.BASH_ENVpointing at a file containingset -xis what propagates.BASH_XTRACEFDdoes 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 inrun.logand the lastchecks=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.
