Shell Scripting SED Branching Advanced May 2026

Shell Scripting Advanced SED: Branching & Labels

Control SED flow with b (unconditional branch), t (branch on successful substitution), T (branch on failed substitution), and :label targets. Build loops, if-else logic, and multi-step transformation pipelines in a single sed invocation.

SED's branching commands make it Turing-complete. While most SED scripts are linear, labels and branches let you build loops, skip processing blocks conditionally, and implement if-else logic — all inside a single stream pass. Understanding branching transforms SED from a find-replace tool into a full text transformation engine.

BASH
# :label  — define a branch target (no output, no state change)
# b label — unconditional jump to :label
# b       — jump to end of script (print current pattern space, next cycle)
# t label — jump to :label IF any s/// succeeded since last line/t
# T label — jump to :label IF no s/// succeeded since last line/T (GNU)

# ── Skip to end (b without label) ────────────────────────
sed '/^#/b                 # skip rest of script for comment lines
s/old/new/g
s/foo/bar/g'

# ── Conditional skip: if-else ─────────────────────────────
sed '/ERROR/{
  s/ERROR/CRITICAL/
  b done          # skip WARN processing
}
/WARN/{
  s/WARN/WARNING/
}
:done'

# ── Loop with t — substitute until stable ────────────────
sed ':loop
s/  / /         # collapse two spaces to one
t loop'         # if s succeeded, loop; else stop
# Result: any run of spaces becomes a single space

# ── Loop with b — unconditional ───────────────────────────
sed ':top
/\\$/{            # if line ends with backslash
  N               # append next line
  s/\\\n//        # remove backslash + newline
  b top           # loop back to check for more
}'
# Joins all backslash-continued lines

# ── T — branch on substitution FAILURE (GNU sed) ──────────
sed 's/^[[:space:]]*/&/   # attempt trim (always succeeds trivially)
T skip            # branch if no substitution: won'\''t fire here
s/ \+/ /g        # normalise spaces
:skip'
BASH
# ── Remove all occurrences of nested brackets ─────────────
sed ':loop
s/([^()]*)//   # remove innermost () pair
t loop'         # repeat until no more () remain
# Input:  func(a(b(c)))  → Output: func

# ── Strip all HTML tags ───────────────────────────────────
sed ':loop
s/<[^>]*>//g
t loop'         # handles tags that span lines when combined with N

# ── Collapse all blank lines to one ──────────────────────
sed '/^$/{ N; /^\n$/d; P; D }'
# If blank line: read next; if still blank: delete; else P+D

# ── Convert tab-indented tree to dash-indented ────────────
sed ':loop
s/^\t/  - /      # first tab becomes "  - "
t loop'          # loop: replace each leading tab

# ── Process lines until a sentinel ────────────────────────
sed '/^STOP$/{ b end }
s/old/new/g
b
:end
q'              # stop processing at STOP line

# ── Print only changed lines ──────────────────────────────
sed -n 's/ERROR/FIXED/p'  # p flag: print only if substitution happened

# ── Multi-step transform with branch guard ────────────────
sed -E '/^[[:space:]]*$/b    # skip blank lines
/^#/b                        # skip comment lines
s/\bfoo\b/bar/g              # normalise tokens
s/\bFOO\b/BAR/g
s/[[:space:]]+/ /g           # normalise spaces
s/[[:space:]]*$//            # strip trailing whitespace'
BASH
# SED has no native if-else, but you can simulate it with
# address negation (!) and branching

# ── Pattern: if match → do A; else → do B ─────────────────
sed '/pattern/{
  s/old/new/
  b end
}
# else: (lines that did NOT match /pattern/)
s/other/thing/
:end'

# ── Three-way branch: ERROR, WARN, INFO ───────────────────
sed '/\bERROR\b/{
  s/^/[CRITICAL] /
  b done
}
/\bWARN\b/{
  s/^/[WARNING] /
  b done
}
/\bINFO\b/{
  s/^/[INFO]     /
}
:done' app.log

# ── Process only lines matching a count condition ─────────
# Apply substitution only to first 3 ERRORs found
sed '/ERROR/{
  0~3!{ b }      # branch (skip) unless 3rd, 6th, 9th... match
  s/ERROR/SAMPLED_ERROR/
}'

# ── Simulate if line N matches, transform line N+1 ────────
sed '/^HEADER$/{
  n                    # read next line into pattern space
  s/old_value/new_value/
}'
BASH
# ── Idempotent config update ──────────────────────────────
# Only set DB_HOST if it doesn't already have the right value
sed -i '/^DB_HOST=prod-db-01$/b   # already correct, skip
         s/^DB_HOST=.*/DB_HOST=prod-db-01/' config.env

# ── Environment-specific substitution ────────────────────
ENV="production"
sed "
/^APP_ENV=/{
  s/=.*/=${ENV}/
  b done
}
/^LOG_LEVEL=/{
  s/=.*/=$([ \"${ENV}\" = production ] && echo WARN || echo DEBUG)/
}
:done" config.env

# ── Remove C-style comments with loop ─────────────────────
sed ':loop
s|/\*[^*]*\*\+\([^/*][^*]*\*\+\)*/||g
t loop
/\/\*/{ N; b loop }'

# ── Complete SED script: clean a config file ─────────────
sed -E '
/^[[:space:]]*#/b       # keep comment lines as-is
/^[[:space:]]*$/d       # delete blank lines
:norm
s/[[:space:]]+/ /g      # normalise internal spaces
s/^[[:space:]]+//       # strip leading space
s/[[:space:]]+$//       # strip trailing space
/= *$/{                 # value-less keys
  s/=$/= /
  b norm
}' messy.conf
sed — branching demo
vriddh@prod-01:~/scripts$echo "a b c d" | sed ':l; s/ / /; tl'
a b c d
vriddh@prod-01:~/scripts$printf 'ERROR db down\nWARN slow query\nINFO startup\n' | sed '/\bERROR\b/{s/^/[CRITICAL] /; b done}; /\bWARN\b/{s/^/[WARNING] /; b done}; s/^/[INFO] /; :done'
[CRITICAL] ERROR db down
[WARNING] WARN slow query
[INFO] INFO startup
✔ Branching rules — Use b without a label to skip to end of script (print and next cycle). Use t label to loop after successful substitution. Reset the t flag by starting a new cycle or explicitly with a no-op s/$/$/. Prefer address-based guards (/pattern/b) over complex branching when possible — they're more readable. When branching logic grows beyond 5-6 commands, switch to AWK.