sed Part II · Module 6 · Advanced sed Processing
Branching and Conditional Execution
Every script so far has run top to bottom, once per line. This chapter adds the two commands that change that: b jumps to a label, and t jumps only if the last substitution actually changed something. Together they give you a loop — repeat a substitution until it stops finding anything — which is how you squeeze arbitrary runs of whitespace, or join an unknown number of continuation lines. It is also the chapter with the most dangerous portability trap in this track, and not because the trap is obscure: the compact one-line form that nearly every example online uses runs on BSD sed, prints a warning you will not see, leaves your file completely unchanged, and exits with status zero.
Advanced21 min readsedPart IIControl FlowPortabilityAdvanced
Part II · Module 6 · Chapter 6.3 · Branching and Conditional Execution
1. Three Pieces
:name a LABEL — marks a place in the script. Does nothing by itself.
b name BRANCH — jump to that label, always.
t name TEST — jump to that label ONLY IF the last s changed something.
b a bare b — jump to the END of the script, skipping the rest.t is the useful one. It is the only command in sed that asks "did that work?", and it is what lets a script repeat an operation an unknown number of times.
A label is just a marker. :squeeze creates no behaviour; it only gives b and t somewhere to jump to.
2. Writing It So It Works
Before any examples, the syntax, because getting this wrong is the subject of section 4.
On BSD sed — which is what macOS ships — a label name extends to the end of the line, and so does the target of a b or t. That means a label and the command after it cannot share a line. There are two correct forms:
sed -e ':a' -e 's/ / /' -e 'ta' file one -e per line sed -f script.sed file a script file, one command per lineBoth work everywhere, GNU included. Everything in this chapter uses one of the two.
3. t — Branch If the Substitution Worked
A two-arm demonstration. The script tries a substitution; if it worked, it jumps past a marker, and if it did not, the marker gets added:
printf 'aaa\nbbb\n' | sed -e 's/a/X/' -e 'tdone' -e 's/^/NO-SUB: /' -e ':done'Xaa
NO-SUB: bbbBoth arms, in one run:
aaa—s/a/X/changed it toXaa, sotdonejumped to the label and the marker command was skipped.bbb— the substitution found noa, sotdonedid not jump, execution continued, and the marker was prepended.
That is t in full. Two rules make it predictable, and both matter for loops:
- The "did a substitution succeed" flag is reset when a new line is read, so each line starts fresh.
- It is also reset when a
tactually jumps. So a loop'sttests only whether the substitution inside this iteration worked — which is what makes the loop stop rather than spinning forever.
4. The Compact Form, and Why It Is Dangerous Here
Now the trap. The registered example for this chapter is written in the compact GNU style:
sed ':a; s/[[:space:]][[:space:]]/ /; ta' sta/timing.rptOn GNU sed that squeezes every run of spaces down to one. On the BSD sed these examples were verified against, here is what happens. First, standard error:
sed: 2: ":a; s/[[:space:]][[:spa ...": unused label 'a; s/[[:space:]][[:space:]]/ /; ta'Read the label name in that message: a; s/[[:space:]][[:space:]]/ /; ta. The whole rest of the line became the name of the label, because that is the BSD rule from section 2. So the script contained one label and no commands at all.
Second, standard output — nine lines, the file completely unchanged. Counting the lines that still contain two consecutive spaces:
grep -c ' ' <output of the command above>7Seven lines still have double spaces. And third, the exit status:
0A warning on stderr, the input passed through untouched, and a success status. Compare that with the portable form:
sed -e ':a' -e 's/[[:space:]][[:space:]]/ /' -e 'ta' sta/timing.rptStartpoint: u_alu/rd_data_reg[3]
Endpoint: u_fifo/wr_ptr_reg[0]
data arrival time 1.842
data required time 2.100
slack (MET) 0.258
Startpoint: u_fifo/rd_ptr_reg[1]
Endpoint: u_alu/op_reg[2]
data arrival time 2.410
slack (VIOLATED) -0.110Zero lines with double spaces. The two commands are supposed to do the same thing and differ by nothing but line breaks.
The bare-b one-liner fails differently, and more usefully:
sed '/^Startpoint/,/^[[:space:]]*slack/!b; s/^/ /' sta/timing.rptsed: 2: "/^Startpoint/,/^[[:spac ...": undefined label '; s/^/ /'Exit status 1 — nothing is produced, which you cannot miss. Same syntax rule, opposite visibility: here the swallowed text became a branch target that does not exist, so sed refused to run; in the :a case it became a label definition, which is harmless and therefore silent.
5. The Loop That Squeezes Whitespace
A single substitution with g replaces every non-overlapping match once. That is not the same as repeating until nothing changes — four consecutive spaces contain two non-overlapping pairs, so one pass with g turns them into two spaces, not one. Repeating is what finishes the job, and t is how you repeat.
As a script file, which is where this belongs:
cat -n scripts/squeeze.sed 1 :a
2 s/[[:space:]][[:space:]]/ /
3 tased -f scripts/squeeze.sed sta/timing.rptStartpoint: u_alu/rd_data_reg[3]
Endpoint: u_fifo/wr_ptr_reg[0]
data arrival time 1.842
data required time 2.100
slack (MET) 0.258
Startpoint: u_fifo/rd_ptr_reg[1]
Endpoint: u_alu/op_reg[2]
data arrival time 2.410
slack (VIOLATED) -0.110Every column of padding collapsed to one space. On the data arrival time line that is around thirty spaces reduced by about twenty-nine successive passes, all driven by three lines of script.
This is the standard normalisation step before diffing two tool reports: with the padding gone, a diff shows you the values that changed rather than the columns that moved. Chapter 6.4 builds exactly this into a reusable script file.
6. A Bare b — Skip the Rest
b with no label jumps to the end of the script. Combined with a negated address it reads as "unless this line is interesting, stop here" — a guard clause:
cat -n sta/report.rpt 1 Report : timing
2 Design : top
3 Startpoint: u_alu/rd_data_reg[3]
4 Endpoint: u_fifo/wr_ptr_reg[0]
5 slack (MET) 0.258
6 1 path reportedsed -e '/^Startpoint/,/^[[:space:]]*slack/!b' -e 's/^/ /' sta/report.rptReport : timing
Design : top
Startpoint: u_alu/rd_data_reg[3]
Endpoint: u_fifo/wr_ptr_reg[0]
slack (MET) 0.258
1 path reportedLines 3–5 gained two spaces; the header and footer did not. Without the guard, everything is indented:
sed -e 's/^/ /' sta/report.rpt Report : timing
Design : top
Startpoint: u_alu/rd_data_reg[3]
Endpoint: u_fifo/wr_ptr_reg[0]
slack (MET) 0.258
1 path reported7. Think It Through
1. What is the difference between b and t?
b always jumps. t jumps only if a substitution has changed the pattern space since the last line was read or the last successful t. b is for skipping; t is for looping and for asking "did that work".
2. Write a portable command that collapses runs of two or more spaces into one.
sed -e ':a' -e 's/ / /' -e 'ta' file, or the same three lines in a -f script. The label and the commands must be on separate -e fragments or separate lines — section 4 is what happens otherwise. Note s/ / /g alone is not equivalent: with g it handles non-overlapping pairs in one pass, so four spaces become two rather than one.
3. A colleague's normalisation step "runs fine" and the two reports still differ on every line. Their script is sed ':a; s/ / /; ta'. What do you tell them?
That on BSD sed the entire script became a label name, so nothing was substituted — and the only evidence was a stderr warning, because stdout was the unchanged file and the exit status was 0. Rewrite it with one -e per command or move it to a -f file. Then check with grep -c ' ' rather than by eye.
4. Why does a t loop terminate?
Because the flag is reset each time the branch is taken, so each pass tests only its own substitution — and because the substitution shrinks the line. When no pair of spaces is left, that pass changes nothing, t does not jump, and the script falls through. A loop whose substitution does not shrink the line is not guaranteed to stop.
5. When is a bare b better than putting an address on the command?
When more than one command should be skipped. For one command, /addr/s/…/…/ is clearer. For four, /addr/!b at the top is a guard clause that applies to all of them without wrapping them in a block — though a { } block from Chapter 5.4 is an equally good answer and often easier to read.
6. You need to join every continuation line onto its parent, however many there are. Sketch it.
A loop around the N from Chapter 6.2: label, then $!N, then a substitution that joins a newline-plus-indent, then t back to the label — so the window keeps growing while continuations keep being found. In a script file:
:join
$!N
s/\n[[:space:]]\{4,\}/ /
tjoin
P
DThat is the general form of Chapter 6.2's fixed two-line version. It is also about as complex as a sed script should get before awk becomes the better tool.
8. Summary
- A label marks a place;
bjumps there always;tjumps only if the last substitution changed something. A barebjumps to the end of the script. tis the only conditional in sed, and the flag resets when a new line is read and when a branch is taken — which is why loops terminate.- On BSD sed a label name runs to the end of the line. A label and a command cannot share one.
- The compact
':a; s/…/; ta'form is the worst trap in this track: a stderr warning, the input unchanged on stdout, and exit status 0. Seven lines still had double spaces where the portable form left none. - The bare-
bone-liner fails loudly instead —undefined label, exit 1 — from the same syntax rule. The difference in visibility is luck, not design. - Write one
-eper command, or use a-fscript file. This is a structural fix, not a matter of care. - A
tloop is how you repeat until done.s/ / /gin one pass is not the same thing: four spaces become two, not one. - A loop's substitution must shrink the line or the loop may never end.
- A guard clause is
/addr/!b— and testing it needs a fixture with lines outside the range, or the guarded and unguarded versions look identical.
Three chapters of this module have now ended with the same recommendation: put the script in a file. A hold-space pairing, a sliding window and a t loop are all past the point where a one-liner is reviewable, and branching in particular is only reliably writable that way. Chapter 6.4 — sed Script Files and Reusable Transformations closes Module 6 by treating the script as what it has become — flow code that belongs in version control, with comments, next to the thing it transforms.
Where this fits
Part of the sed curriculum.
