Regular Expressions in Bash

Chapter 8 — sed and Regex in Depth

The sed Substitute Command

The s command is the heart of sed. It finds a regex match on a line and replaces it with a replacement string. Understanding every part of its syntax — address, pattern, replacement, and flags — is what makes sed a powerful text transformation engine.

3,/end/s/pattern/replacement/flags
address regex pattern replacement text flags
  • Address — which lines to operate on (optional; if omitted, all lines)
  • Pattern — BRE by default; ERE with sed -E
  • Replacement — literal text plus special tokens (&, \1\9, case modifiers)
  • Flags — modify how the substitution behaves (g, i, p, w, Nth)
  • Delimiter/ is conventional but any character works: s|/usr|/opt|g

BRE vs ERE in sed

By default sed uses BRE. The -E flag (GNU sed; also -r on some systems) switches to ERE — the same extended syntax that grep -E uses.

# BRE (default) — grouping and + need backslashes
sed 's/\(foo\)\+/bar/g' file.txt

# ERE with -E — cleaner syntax, + and () work directly
sed -E 's/(foo)+/bar/g' file.txt

# BRE alternation needs \|
sed 's/cat\|dog/pet/g' file.txt

# ERE alternation — just |
sed -E 's/cat|dog/pet/g' file.txt
FeatureBRE (default)ERE (-E)
Grouping\(...\)(...)
One or more\++
Zero or one\??
Alternation\||
Backreference (pattern)\1\1
Backreference (replacement)\1\1
& whole matchYesYes
Use sed -E for new scripts. The ERE syntax is less cluttered and more consistent with grep -E and awk. Reserve BRE-only syntax for scripts that must run on strictly POSIX systems without GNU sed.

Substitution Flags

g Global Replace ALL occurrences on the line, not just the first. The most commonly used flag.
i / I Case insensitive Match the pattern without regard to letter case. GNU sed extension.
N (number) Nth occurrence Replace only the Nth match on the line, e.g. 2 replaces the second match only.
Ng Nth and beyond Replace from the Nth occurrence onwards. 2g replaces second, third, fourth… matches.
p Print Print the line if a substitution was made. Use with sed -n to print only changed lines.
w file Write Write the substituted line to a file. Creates or truncates the file at start of script.
e Execute Execute the replacement as a shell command and replace the line with its output. GNU only.
m / M Multi-line Makes ^ and $ match at the start/end of each embedded newline in the pattern space.
Combine Mix freely Flags combine: gi = global case-insensitive; 2p = print only if 2nd occurrence replaced.
# g: replace all occurrences (default replaces only the first)
echo "aaa bbb aaa" | sed 's/aaa/xxx/'
xxx bbb aaa        # only first replaced
echo "aaa bbb aaa" | sed 's/aaa/xxx/g'
xxx bbb xxx        # all replaced

# i: case-insensitive replacement
echo "Error ERROR error" | sed 's/error/FAULT/gi'
FAULT FAULT FAULT

# 2: replace only the second occurrence
echo "cat and cat and cat" | sed 's/cat/dog/2'
cat and dog and cat

# 2g: replace from the second occurrence onwards
echo "cat and cat and cat" | sed 's/cat/dog/2g'
cat and dog and dog

# p with -n: print ONLY lines where a substitution was made
sed -n 's/ERROR/FAULT/p' app.log

# w: write substituted lines to a separate file
sed 's/ERROR/FAULT/gw /tmp/faults.txt' app.log

# e: replace line with output of a shell command (GNU sed only)
echo "date" | sed 's/.*/date/e'         # replaces "date" with the output of date(1)

Replacement Special Tokens

& Whole match Inserts the entire matched text. Equivalent to \0 in some flavours.
\1 … \9 Capture group Inserts the text captured by group N. Requires (...) in ERE or \(...\) in BRE.
\\ Literal backslash A literal \ in the output. Use \\ in the replacement to insert a real backslash.
\n Newline in output Inserts a newline into the replacement text, splitting the line in two.
# &: wrap the entire match in brackets
echo "hello world" | sed 's/[a-z]*/[&]/g'
[hello] [world]

# \1 \2: reorder captured groups (date reformatting)
echo "2026-06-11" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
11/06/2026

# Swap first and last name
echo "Smith, John" | sed -E 's/([A-Za-z]+), ([A-Za-z]+)/\2 \1/'
John Smith

# Quote every word
echo "one two three" | sed 's/[^ ]*/"\&"/g'
"one" "two" "three"

# Insert a newline: split "key=value" into two lines
echo "name=Alice" | sed 's/=/\n/'
name
Alice

# Escape & and \ when they must be literal in replacement
echo "foo" | sed 's/foo/a\&b/'     # output: a&b  (\& = literal &)
echo "foo" | sed 's/foo/a\\b/'     # output: a\b  (\\ = literal \)

Case Modifier Sequences (GNU sed)

GNU sed supports case-conversion sequences in the replacement string. These are not available in POSIX sed or BSD sed.

\u Uppercase next Capitalise the next character only.
\l Lowercase next Lowercase the next character only.
\U UPPERCASE rest Uppercase from here to end of replacement or \E.
\L lowercase rest Lowercase from here to end of replacement or \E.
\E End modifier Stop the \U or \L modifier — return to default case.
Case modifier examples s/\b\(.\)/\u\1/g — title-case every word hello world Hello World s/.*/\U&/ — uppercase the entire line hello world HELLO WORLD s/.*/\L&/ — lowercase the entire line HELLO WORLD hello world s/\b\([a-z]\)/\u\1/g — capitalise first letter of each word the quick brown fox The Quick Brown Fox s/\(ERROR\)/\L\1/g — lowercase the match only, then \E stops it found ERROR in log found error in log
# Title-case: capitalise the first letter of every word (BRE)
echo "the quick brown fox" | sed 's/\b\([a-z]\)/\u\1/g'
The Quick Brown Fox

# Title-case with ERE
echo "the quick brown fox" | sed -E 's/\b([a-z])/\u\1/g'
The Quick Brown Fox

# SCREAMING_SNAKE_CASE from lowercase
echo "my variable name" | sed 's/ /_/g; s/.*/\U&/'
MY_VARIABLE_NAME

# Normalise HTTP method names (mixed case → uppercase)
echo "method: Get" | sed -E 's/(GET|POST|PUT|DELETE|PATCH)/\U\1/gi'
method: GET

# camelCase to snake_case: insert _ before each uppercase letter then lowercase all
echo "camelCaseVariable" | sed 's/\([A-Z]\)/_\L\1/g'
camel_case_variable

Address-Targeted Substitution

Addresses restrict which lines a sed command applies to. This is one of sed's most powerful features — you can combine regex addresses with substitution commands to perform context-aware replacements.

N Line number Apply only to line N. 1 = first line; $ = last line.
N,M Line range Apply to lines N through M inclusive.
/regex/ Pattern address Apply to every line that matches the regex.
/start/,/end/ Pattern range Apply from the line matching start to the line matching end, inclusive.
first~step Step address Apply to every Nth line starting at first. 0~2 = even; 1~2 = odd. GNU only.
addr! Negated address Apply to every line that does NOT match the address. /pattern/!s/x/y/
# Line number addresses
sed '1s/^/# File header\n/' file.txt          # add comment before line 1
sed '$s/$/\n# End of file/' file.txt           # add comment after last line
sed '2,5s/^/    /' file.txt                   # indent lines 2–5

# Pattern address: substitute only on lines matching a condition
sed '/^Port/s/22/2222/' /etc/ssh/sshd_config  # change port only on Port lines
sed '/ERROR/s/$/  <<< ALERT/' app.log          # append ALERT to error lines

# Pattern range: substitute within a block
sed '/\[section\]/,/\[end\]/s/^ *//' config.ini # strip leading spaces in section
sed '/^---/,/^---/s/\bFoo\b/Bar/g' file.md    # replace only inside front-matter

# Negated address: substitute on every line EXCEPT those matching
sed '/^#/!s/foo/bar/g' script.sh             # skip comment lines
sed '/^$/!s/^/  /' file.txt                  # indent all non-empty lines

# Step address: number every other line (GNU sed)
sed '1~2s/^/> /' file.txt                   # prefix odd lines with "> "

# Combined: two addresses + substitution in a block
sed '/^BEGIN/,/^END/ { s/\bold\b/new/g; s/foo/bar/g }' file.txt

Alternate Delimiters

The delimiter does not have to be /. Any punctuation character can be used. This avoids the "leaning toothpick" problem when patterns or replacements contain slashes.

# Path substitution — using | as delimiter avoids escaping every /
sed 's|/usr/local|/opt|g' Makefile

# Compare: using / would require escaping all the slashes
sed 's/\/usr\/local/\/opt/g' Makefile   # harder to read

# URL rewriting with # as delimiter
sed 's#https://old.example.com#https://new.example.com#g' links.txt

# Pattern address with alternate delimiter — use \%..%
sed '\%/usr/local%s/local/share/' config.txt

# The rule: whatever character follows s is the delimiter
sed 's,foo,bar,g' file.txt     # comma as delimiter
sed 's@foo@bar@g' file.txt     # @ as delimiter

Practical sed Recipes

Config file management

# Set a key to a new value (handles both "key value" and "key=value" forms)
sed -E 's/^(MaxSessions\s*)=?\s*.*/\1= 20/' sshd_config

# Uncomment a setting: remove leading # from lines matching a key
sed '/^#.*Port /s/^#//' sshd_config

# Comment out a setting
sed 's/^PermitRootLogin/# PermitRootLogin/' sshd_config

# In-place edit with backup (GNU sed)
sed -i.bak 's/^Port 22/Port 2222/' /etc/ssh/sshd_config

Data reformatting

# ISO date (YYYY-MM-DD) to US format (MM/DD/YYYY)
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\2\/\3\/\1/g' dates.txt

# CSV: quote unquoted fields (add quotes around each comma-separated value)
sed -E 's/([^,]+)/"\1"/g' data.csv

# Remove ANSI colour escape codes from terminal output
sed -E 's/\x1B\[[0-9;]*[mK]//g' coloured.log

# Normalise multiple spaces to one
sed 's/  */ /g' file.txt           # BRE: two or more spaces → one space
sed -E 's/ {2,}/ /g' file.txt     # ERE: cleaner syntax

# Strip leading and trailing whitespace
sed 's/^[[:space:]]*//; s/[[:space:]]*$//' file.txt

# Extract value from "key: value" format (print only the value)
sed -n '/^Name:/s/^Name:[[:space:]]*//'p record.txt

Log and source code manipulation

# Mask sensitive data: replace password values
sed -E 's/(password=)[^ &]*/\1[REDACTED]/gi' app.log

# Add line numbers as prefix
sed = file.txt | sed 'N; s/\n/\t/'

# Delete blank lines
sed '/^[[:space:]]*$/d' file.txt

# Delete comments (lines starting with #)
sed '/^[[:space:]]*#/d' config.txt

# Rename a variable across a source file
sed -i 's/\bconnection_timeout\b/connect_timeout/g' config.py

Portability — GNU sed vs BSD sed

FeatureGNU sed (Linux)BSD sed (macOS)
-E flagYesYes (BSD also accepts -E)
-r flag (same as -E)YesNo
-i in-placesed -i or sed -i.baksed -i '' or sed -i .bak
Case modifiers (\u \U \l \L)YesNo
i/I flag (case insensitive)YesNo
Nth occurrence flagYesYes
e flag (execute)YesNo
Step address (first~step)YesNo
\w shorthand in regexYesNo — use [[:alnum:]_]
\b word boundary in regexYesNo — use \<\>
macOS -i gotcha: BSD sed requires a space between -i and the suffix argument: sed -i .bak '...' or sed -i '' '...' (empty string for no backup). GNU sed accepts sed -i.bak with no space. To write scripts that work on both, use: sed -i.bak '...' — the .bak immediately after -i works on both GNU and BSD.

Quick Reference — Chapter 8

Substitution Syntax

s/pat/rep/Replace first match on each line
s/pat/rep/gReplace all matches on each line
s/pat/rep/2Replace only the 2nd occurrence
s/pat/rep/giAll matches, case-insensitive (GNU)
s/pat/rep/pPrint if substitution made (use with -n)

Replacement Tokens

&Entire matched text
\1 … \9Capture group back-reference
\u\1Capitalise first char of group 1
\U&Uppercase the entire match
\L&Lowercase the entire match
\nInsert newline in replacement

Common Address Patterns

1s/...First line only
$s/...Last line only
/regex/s/...Lines matching regex
/start/,/end/s/...Lines in a block
/regex/!s/...Lines NOT matching regex
What is coming next: Chapter 9 covers regex in awk — pattern-action rules, /regex/ { action }, field matching with $0$NF, the match() function, sub() and gsub(), capturing groups in gawk, and practical awk recipes that combine regex with arithmetic and formatted output.