Linux Basic — sed in Scripts
sed in Scripts
sed (“stream editor”) shines at line-by-line text transforms: substitutions, deletions, inserts. In a script, it’s the tool you reach for when you need to edit a file or stream and parameter expansion isn’t enough.
This page focuses on the four sed patterns that show up in nearly every script.
Pattern 1 — Substitute
echo "hello world" | sed 's/world/Bash/'
# hello Bash
Flags:
| Flag | Meaning |
|---|---|
g |
global — every match on the line |
i |
case-insensitive (GNU; BSD: I) |
2 |
only the 2nd match |
p (with -n) |
print only changed lines |
echo "aaa bbb aaa" | sed 's/aaa/X/' # X bbb aaa
echo "aaa bbb aaa" | sed 's/aaa/X/g' # X bbb X
echo "aaa bbb aaa" | sed 's/aaa/X/2' # aaa bbb X
echo "ABC" | sed 's/abc/x/I' # x (case-insensitive)
Use extended regex with -E (portable on GNU + BSD) instead of \(...\):
echo "user42" | sed -E 's/^([a-z]+)([0-9]+)$/\2-\1/'
# 42-user
Pattern 2 — Pick a Different Delimiter
The default delimiter is /. If your pattern contains slashes (URLs, paths), pick another:
url="https://example.com/api"
sed "s|$url|REDACTED|" file.txt
| , # : all work. Just be consistent on both sides.
Pattern 3 — In-Place Edit (-i) — The macOS Trap
sed -i 's/old/new/g' file.txt
This works on GNU sed (Linux). On BSD sed (macOS by default), -i requires an explicit backup-suffix argument:
# GNU (Linux)
sed -i 's/old/new/g' file.txt
# BSD (macOS)
sed -i '' 's/old/new/g' file.txt # empty string = no backup
sed -i .bak 's/old/new/g' file.txt # creates file.txt.bak
A portable form for cross-platform scripts:
case "$OSTYPE" in
darwin*) sedi=(sed -i '') ;;
*) sedi=(sed -i) ;;
esac
"${sedi[@]}" 's/old/new/g' file.txt
Or always create a backup and clean it up:
sed -i.bak 's/old/new/g' file.txt
rm file.txt.bak
Detect once, reuse the wrapper everywhere.
Pattern 4 — Delete, Insert, Print
| Command | Action |
|---|---|
Nd |
delete line N |
/regex/d |
delete matching lines |
N,Md |
delete range N through M |
Na text |
append text AFTER line N |
Ni text |
insert text BEFORE line N |
Nc text |
replace line N with text |
-n 'Np' |
print only line N |
-n '/regex/p' |
print only matching lines |
# Delete comments and blank lines
sed -E '/^[[:space:]]*#/d; /^[[:space:]]*$/d' config.txt
# Print lines 10-20
sed -n '10,20p' file.txt
# Insert a header at the top of a file
sed -i '1i\#!/usr/bin/env bash' file.sh
# Add a shebang only if missing (GNU)
grep -q '^#!' file.sh || sed -i '1i\#!/usr/bin/env bash' file.sh
The \ at the end of 1i\ is required on BSD; GNU accepts both.
Multi-Command Scripts — -e and ;
sed -e 's/foo/FOO/g' -e 's/bar/BAR/g' file.txt
# or, more compactly
sed 's/foo/FOO/g; s/bar/BAR/g' file.txt
For longer scripts, use a here-doc:
sed -E -f - file.txt <<'SCRIPT'
s/foo/FOO/g
s/bar/BAR/g
/^DEBUG/d
SCRIPT
'SCRIPT' (single-quoted) prevents Bash from expanding $1, etc., inside the sed program.
Address Ranges
| Address | Matches |
|---|---|
N |
line N |
$ |
last line |
N,M |
lines N–M |
/start/,/end/ |
from /start/ line through /end/ line |
N~M |
every M-th line starting at N (GNU only) |
0~2 |
every other line starting from 1 (GNU) |
# Print everything between BEGIN and END markers
sed -n '/^BEGIN$/,/^END$/p' file.txt
# Delete every 2nd line (GNU)
sed '0~2d' file.txt
Substitution with Bash Variables
old="API_KEY"
new="REDACTED"
# ✅ Use double quotes so $old / $new expand
sed -i "s/${old}/${new}/g" file.txt
Caveats:
- If
$oldor$newcontains the delimiter (/), pick another (|) - If
$oldcontains regex meta-chars, escape them
A safe helper for “escape for sed BRE”:
sed_escape() {
printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'
}
old=$(sed_escape "https://example.com/api")
sed -i "s/$old/REDACTED/g" file.txt
For “escape for the replacement side” too, replace [\/&] with [\/&] and additionally escape \ — the helper above already covers it.
In-Place Edit with a Backup Always
A safer pattern than blind -i:
edit_in_place() {
local file=$1
shift
cp -p "$file" "$file.bak.$$"
if "$@" < "$file.bak.$$" > "$file"; then
rm "$file.bak.$$"
else
mv "$file.bak.$$" "$file" # restore on failure
return 1
fi
}
edit_in_place /etc/hosts.local sed 's/old/new/g'
No reliance on platform -i behaviour, and you keep the original if sed fails.
When to Choose awk Instead
Use awk (next page) instead of sed when you need:
- Column-oriented data (CSV, log fields,
ps/df/lsoutput) - Aggregation (sums, counts, averages)
- Conditional logic beyond “this line matches”
- Multi-line state (counters, running totals)
sed is at its best when each line is transformed independently.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
sed -i 's/x/y/' file on macOS |
sed -i '' 's/x/y/' file or use the wrapper |
BSD -i requires a suffix |
's/\d+/N/' (Perl regex) |
-E 's/[0-9]+/N/' |
sed uses POSIX, not PCRE |
Hardcoded / in the URL pattern |
use alternative delimiter | # |
escape hell otherwise |
Single-quote a command containing $var |
double-quote with care | single quotes don’t expand vars |
Many seds piped together |
one sed with -e (or awk) |
each pipe is a fork |
Next → awk in Scripts