Go with the Flow?

Learning SED

Chapter 8 — Branching and Labels

Control Flow in SED

SED scripts normally execute top to bottom — command 1, command 2, command 3, and so on through to the end of the script for each line of input. Branching breaks that linearity. It lets you jump to a named point in the script (a label), skip sections, or loop back to repeat commands. This turns SED from a simple transformation pipeline into a genuine programming language — limited, but Turing-complete.

There are three branch commands and one label declaration:

:label
Label declaration
Marks a position in the script. Not a command — just a named target that branch commands can jump to.
b
Unconditional branch
Jump to a label immediately. Always fires. If no label given, jumps to end of script.
t
Branch if substitution succeeded
Jump to a label only if a successful s command has been made since the last line was read or since the last t.

GNU sed adds a fourth:

Command Fires when… POSIX?
:label Always — it is a target, not a command Yes
b label Always (unconditional) Yes
b (no label) Always — jumps to end of script (skips remaining commands) Yes
t label Only if a s command succeeded since last line read or last t Yes
T label Only if NO s command has succeeded since last line read or last t/T GNU sed only

Labels: :name

A label is declared with a colon followed immediately by a name — no space between them:

:myloop          # declares a label named "myloop"
:a               # single-letter labels are conventional for brevity
:top             # descriptive names work too
:retry

Label names can contain letters, digits, and underscores. Single-letter labels (:a, :b, :l) are the convention in most SED scripts because scripts are often written as compact one-liners. In -f script files, longer names improve readability.

Labels are not commands. They produce no output, change nothing in the pattern or hold space, and are completely invisible to the input. They exist only as jump targets for b, t, and T.

The b Command — Unconditional Branch

b label jumps execution to the named label immediately, regardless of any conditions. It always fires. b with no label name jumps to the end of the script, which triggers the default print and starts the next cycle.

Skipping the rest of the script

# Process lines matching "KEEP" specially; skip all other commands for other lines
sed '/KEEP/! b          # not a KEEP line — skip to end of script (default print)
s/KEEP://              # remove the KEEP: prefix
s/^/  > /             # indent it
' file.txt
Flow for each line: /KEEP/! matches? → yes (not a KEEP line) → b fires → jump to end → default print → next cycle → no (IS a KEEP line) → continue: s/KEEP:// → s/^/ > / → print

Using b in a multi-branch structure

# Route lines to different processing branches based on content
sed '/^ERROR/   { s/^/[FAULT]  /; b end }
/^WARNING/ { s/^/[ALERT]  /; b end }
/^INFO/    { s/^/[NOTICE] /; b end }
/^DEBUG/   d
:end' logfile.txt
# Each branch transforms the line and jumps to :end to avoid
# falling through into subsequent branches

Jumping forward to skip a block

# Skip lines 10–20 from a transformation but still print them
sed '10,20 b skip
s/foo/bar/g
:skip' file.txt
# Lines 10–20: b fires, jumps over s/foo/bar/, still printed by default
# All other lines: s/foo/bar/ runs normally

The t Command — Branch If Substitution Succeeded

t is the conditional branch — it checks an internal flag that SED sets whenever an s command makes a successful substitution. If the flag is set, t label jumps to the label and clears the flag. If the flag is not set (no substitution has succeeded), t does nothing.

The flag is also cleared automatically at the start of each new cycle (each new line read) and after each t or T command fires.

t label — decision logic: Has any s succeeded since last line read or last t/T? → YES: jump to label, clear the flag → NO: do nothing, continue to next command

The loop pattern — the most important use of t

The most common use of t is creating a loop: place a label before a substitution, and after the substitution use t to branch back to the label. The loop runs as long as the substitution keeps succeeding:

# Remove all leading spaces one at a time (loop until no leading space left)
sed ':loop
s/^ //
t loop' file.txt

# More efficient — but the loop pattern makes the logic visible:
sed 's/^ *//' file.txt  # same result in one step

Global replacement with controlled repetition

# Replace "aa" with "a" repeatedly until no more double-a pairs remain
sed ':a; s/aa/a/; ta' file.txt
# Input: "aaaa"
# s replaces "aa" → "aaa" (succeeded) → t branches back
# s replaces "aa" → "aa"  (succeeded) → t branches back
# s replaces "aa" → "a"   (succeeded) → t branches back
# s finds no "aa" → fails  → t does NOT branch → end of script
# Output: "a"

Joining continuation lines — the classic t loop

# Collapse lines ending with backslash onto the next line (Makefile/shell style)
sed ':a
/\\$/ {
  N
  s/\\\n//
  ta
}' file.txt
# While the current accumulated line ends with \:
#   N appends the next line
#   s removes the backslash-newline join
#   t loops if the substitution succeeded (i.e. there was a backslash-newline)

Removing nested brackets

# Remove all content inside parentheses, including nested ones
sed ':a; s/([^()]*)//; ta' file.txt
# [^()]* matches content that contains no brackets (innermost pair)
# Removes the innermost pair each iteration until none remain
# Input:  "result (foo (bar) baz) end"
# Pass 1: s removes "(bar)"       → "result (foo  baz) end"
# Pass 2: s removes "(foo  baz)"  → "result  end"
# Pass 3: s finds nothing to remove → t fails → done

Trimming both leading and trailing whitespace in one clean loop

sed ':a; s/^[[:space:]]*//; s/[[:space:]]*$//; ta' file.txt
# Loops until neither substitution makes any change
# In practice, both substitutions succeed at most once per line
# — but the loop guarantees correctness even for pathological input

The T Command — Branch If Substitution Failed

T (GNU sed only) is the logical inverse of t. It branches to the label if no successful substitution has occurred since the last line was read or the last t/T. Where t says "keep looping while things are changing", T says "jump away if nothing matched".

# Process a line only if a substitution succeeded; skip it otherwise
sed 's/foo/bar/; T skip; s/bar/BAR/
:skip' file.txt
# If s/foo/bar/ succeeded: T does NOT fire, s/bar/BAR/ runs → "BAR"
# If s/foo/bar/ failed:    T fires → jumps to :skip, s/bar/BAR/ never runs
# Use T to handle "no match" — the else branch of an if-else structure
sed 's/ERROR/FAULT/
T noerror
s/$/  [FLAGGED]/
b done
:noerror
s/$/  [OK]/
:done' logfile.txt
# Lines with ERROR: substitution succeeds → T skips → append [FLAGGED]
# Lines without ERROR: substitution fails → T fires → append [OK]

Building If-Else Logic with Branching

SED has no native if-else syntax, but you can construct equivalent logic with addresses, b, t, and T. Here are the standard patterns:

Pattern: if-then (address guard)

# "if line matches pattern, do X"
sed '/pattern/ { commands }' file.txt
# The address restricts the block — no branching needed for simple cases

Pattern: if-then-else using b

sed '/pattern/ {
  # THEN branch
  s/foo/bar/
  b done
}
# ELSE branch
s/foo/baz/
:done' file.txt

Pattern: if-then-else using t / T

sed 's/pattern/replacement/   # attempt the substitution
T else                         # if it FAILED, jump to else
# THEN branch — substitution succeeded
s/replacement/DONE/
b end
:else
# ELSE branch — substitution failed
s/$/  [no match]/
:end' file.txt

Pattern: if NOT using negated address

sed '/pattern/! {
  # runs only on lines that do NOT match
  s/foo/bar/
}' file.txt

Step-by-Step Traces

Trace: Removing nested parentheses

Script: sed ':a; s/([^()]*)//; ta'   Input: sum (x (y+1) z)

Cycle 1 — PS = "sum (x (y+1) z)" :a label — no action s/([^()]*)// → matches "(y+1)" (innermost, no brackets inside) PS = "sum (x z)" flag SET ta flag is set → branch back to :a, flag cleared :a (back at top) s/([^()]*)// → matches "(x z)" PS = "sum " flag SET ta flag is set → branch back to :a, flag cleared :a (back at top) s/([^()]*)// → no match (no parentheses left) flag NOT set ta flag is not set → does not branch → fall through to end Output: "sum "

Trace: t flag reset between cycles

This trace shows the critical point that t's flag is reset with every new line:

Script: sed 's/a/A/; t done; s/b/B/; :done' Input: "ab" / "xy" Cycle 1 — read "ab" (flag starts clear) s/a/A/ → PS="Ab" flag SET t done → flag set → branch to :done (s/b/B/ is skipped) :done → default print → "Ab" Cycle 2 — read "xy" (flag RESET at start of new cycle) s/a/A/ → no match, PS="xy" flag NOT set t done → flag not set → does NOT branch s/b/B/ → no match, PS="xy" :done → default print → "xy"

Loops in Practice

Converting tabs to spaces (expanding tabs)

# Replace each leading tab with 4 spaces, loop until none remain
sed ':a; s/^\t/    /; ta' file.txt

Collapsing multiple spaces into one

sed ':a; s/  / /; ta' file.txt
# Replaces pairs of spaces with one, loops until no pairs remain
# More efficient equivalent: sed 's/  */ /g' (but the loop form is instructive)

Padding a number to fixed width

# Left-pad numbers with zeros to 6 digits
sed -E ':a; s/^([0-9]{1,5})$/0\1/; ta' numbers.txt
# Prepends a zero while the number is less than 6 digits long
# 42 → 042 → 0042 → 00042 → 000042 (stops when {1,5} no longer matches)

Removing all XML/HTML tags from a file

# Handle tags that might span lines (multi-line tags)
sed ':a; s/<[^>]*>//g; /</{ N; ba }' page.html
# First: remove all complete tags on the current line
# If a & lt; remains (incomplete tag), accumulate next line and loop

Building a while-loop: process until a condition is met

# Keep appending lines until we see a line ending with a semicolon
sed -n ':a
/;$/ { p; d }          # found terminator: print accumulated and delete
N                      # not done yet: append next line
ba' statements.txt
# Accumulates lines until one ends with ; then prints the whole statement

Practical Recipes

Comment out lines matching a pattern (idempotent)

# Add # to lines matching "debug" only if they are not already commented
sed '/^#/ b            # already a comment — skip
/debug/ s/^/#/' config.sh

Replace only the second occurrence of a pattern on each line

# Substitute the Nth occurrence cleanly using a loop to skip earlier ones
# Skip the first match (replace it with a marker), replace second, restore first
sed 's/foo/\x00/; s/foo/bar/; s/\x00/foo/' file.txt
# \x00 (null byte) as a temporary marker — won't appear in normal text

In-place toggle: switch a config value between two states

# Toggle debug=true ↔ debug=false
sed 's/debug=true/debug=TOGGLE/; t done
s/debug=false/debug=true/; t done
s/debug=TOGGLE/debug=false/
:done' app.conf

Process only lines in a specific column range

# Add a marker if the 5th field of a colon-delimited file is "active"
sed -E 's/^(([^:]*:){4})active:/\1ACTIVE:/' data.txt
# No branching needed here — but a branch pattern lets you act on it further
sed -E 's/^(([^:]*:){4})active:/\1ACTIVE:/
T skip
s/$/ [ENABLED]/
:skip' data.txt

Duplicate every line that matches a pattern

# Print matching lines twice (original + copy)
sed '/important/ { p; b }
b' file.txt
# p prints first, then b skips to end where default print fires again
# Simpler: sed '/important/p' file.txt (same result)

Multi-pass transformation without a second SED invocation

# Apply three successive transformations in priority order
# Only the first matching transformation fires (fall-through prevented by b)
sed '/CRITICAL/ { s/CRITICAL/[!!!]/; b }
/ERROR/    { s/ERROR/[!!]/;    b }
/WARNING/  { s/WARNING/[!]/;   b }
/INFO/     { s/INFO/[-]/;      b }' logfile.txt

Common Mistakes

Infinite loops

# DANGER: this loop never terminates
# s/^/x/ always succeeds (prepending to start of line always works)
sed ':a; s/^/x/; ta' file.txt
# SED will loop forever, growing the line with x's until killed or OOM

# Fix: ensure the loop has a termination condition
# e.g. stop when line starts with 5 x's:
sed ':a; /^xxxxx/! { s/^/x/; ta }' file.txt

Misunderstanding t's flag scope

# The t flag is shared across ALL s commands in the script for that cycle
# Any successful s anywhere sets the flag — not just the one before t
sed 's/irrelevant/also_irrelevant/  # succeeds on some lines
s/foo/bar/                          # the one we care about
t done                              # fires if EITHER s succeeded!
:done' file.txt
# Fix: use T instead, or restructure so the target s is the only one before t

Branch to a non-existent label

# Branching to an undefined label is an error in most SED implementations
sed 'b nosuchlabel' file.txt
# sed: can't find label for jump to `nosuchlabel'
# Check spelling — label names are case-sensitive

Quick Reference — Chapter 8

Label and Branch Commands

:label Declare a label at this point in the script
b label Unconditional jump to label
b Unconditional jump to end of script (triggers default print, next cycle)
t label Jump to label if any s has succeeded since last line read or last t/T
T label Jump to label if NO s has succeeded (GNU sed only)

Key Patterns

:a; s/pat/rep/; ta Loop: repeat substitution until it stops matching
/pat/ { cmds; b } If-then: run commands and skip rest of script for matching lines
s/p/r/; T else; cmds; b end; :else; cmds2; :end If-else using T (GNU sed)
/p1/{cmds1;b} /p2/{cmds2;b} Multi-branch: route each line to exactly one handler
:a; /\\$/{N;s/\\\n//;ta} Join backslash-continuation lines
:a; s/([^()]*)//; ta Strip nested parentheses iteratively

The t Flag — Rules

Set when Any s command makes a successful substitution
Cleared when A new line is read, or a t or T command fires
Shared across All s commands in the script — any success sets it
What is coming next: Chapter 9 covers the remaining SED commands — y (transliterate), = (line number), r/R (read file), w/W (write file), e (execute), and z (zap/clear pattern space). These complete the full SED command set and round out the toolkit before we move on to regular expressions in depth.