Linux Basic — sed in Scripts

4 minute read

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 $old or $new contains the delimiter (/), pick another (|)
  • If $old contains 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/ls output)
  • 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

Updated: