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:
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.
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
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.
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)
Trace: t flag reset between cycles
This trace shows the critical point that t's flag is reset with every new line:
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
s has succeeded since last line read or last t/T
s has succeeded (GNU sed only)
Key Patterns
The t Flag — Rules
s command makes a successful substitution
t or T command fires
s commands in the script — any success sets it
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.