cheat sheet

sed

Non-interactive text transformation. Substitution, deletion, insertion, address ranges, in-place editing, and multi-line patterns with practical recipes.

sed — Stream Editor

What it is

sed (stream editor) is a POSIX-standard utility included on every Unix and Linux system for performing non-interactive text transformations on a stream or file. It processes input line by line, applying a script of commands — most commonly substitution (s/pattern/replacement/) — and writes the result to stdout, optionally editing files in place with -i. Reach for sed for quick find-and-replace, line deletion, or insertion in scripts and pipelines; for anything involving multiple fields or conditional logic, awk is more readable.

Syntax

A sed script is one or more [address]command pairs; the address selects which lines the command applies to and can be omitted to match every line. Multiple commands can be chained with -e or written in a file passed with -f.

bash
sed [OPTIONS] 'SCRIPT' [FILE...]
sed [OPTIONS] -e 'CMD' -e 'CMD' [FILE...]
sed [OPTIONS] -f script.sed [FILE...]

Output: (none — exits 0 on success)

Common options

OptionMeaning
-nSuppress auto-print (only explicit p prints)
-eAdd an expression
-fRead script from file
-iIn-place edit (GNU: -i'' or -i suffix)
-E / -rExtended regex (no \+, | escaping)
-zNUL-delimited input (for multi-line via NUL)

What's new in GNU sed 4.10

GNU sed 4.10 (released 20 April 2026) is the first stable release since 4.9 (Nov 2022) and rolls up 3.5 years of fixes. The headline items are large-file correctness, a TOCTOU fix for --follow-symlinks -i, and tighter POSIX-mode diagnostics. Nothing in the script language changed — every existing s///, address, and branch label keeps working identically.

ChangeImpact
Lines > 2 GiBGlobal substitutions no longer panic past the 2³¹ byte offset
--follow-symlinks -iTOCTOU race fixed — an attacker can no longer redirect the symlink between resolve and open
Backreferences + $ anchorFalse matches on optional groups followed by end-of-line corrected
\\c\[ in POSIX modeNo longer rejected; backslash-escape after a character class now handled
--debugNo longer crashes when a label is compiled before option processing
BRE a\*\*Accepted as a GNU extension, matching grep's behaviour
DiagnosticsSwitched from grave-accent quoting to apostrophes (`x' → 'x')
POSIX-mode warningNon-portable backslash usage in s/// now produces a warning
BuildCompiles on platforms without <getopt.h>
bash
sed --version | head -1   # confirm 4.10 on Linux
# GNU sed 4.10 — macOS still ships BSD sed; install via: brew install gnu-sed

Output:

text
sed (GNU sed) 4.10

Address forms

Addresses select which lines a command applies to.

AddressMeaning
NLine N
$Last line
N,MLines N through M
N~SEvery S-th line starting from N
/regex/Lines matching regex
/re1/,/re2/From re1-match through re2-match
! suffixNegate the address
0,/re/GNU: from line 0 (first match even on line 1)
bash
sed '5d'           file   # delete line 5
sed '2,4d'         file   # delete lines 2–4
sed '$d'           file   # delete last line
sed '/^#/d'        file   # delete comment lines
sed '1~2d'         file   # delete odd lines (1,3,5,…)
sed '/START/,/END/d' file  # delete range (inclusive)

Output: (none — exits 0 on success)

Given a file with lines 1–6 plus comment lines and a START/END block:

Output:

text
line1
line2
line3
line4
line6

Substitution — s

The s command is the workhorse of sed: it replaces text matching a regex with a replacement string on each addressed line. Without the g flag only the first match on a line is replaced; use & in the replacement to refer to the entire match or \1\9 for capture groups.

bash
s/REGEX/REPLACEMENT/FLAGS

Output: (none — exits 0 on success)

FlagMeaning
gReplace all occurrences on the line
NReplace only the N-th occurrence
iCase-insensitive (GNU)
pPrint line if substitution made
w fileWrite line to file if substitution made
bash
sed 's/foo/bar/'           file   # first occurrence per line
sed 's/foo/bar/g'          file   # all occurrences
sed 's/foo/bar/2'          file   # second occurrence only
sed 's/foo/bar/ig'         file   # case-insensitive, all
sed -n 's/error/ERROR/p'   file   # print only changed lines

Output:

text
bar baz foo
bar baz bar
foo baz bar
bar baz bar
2026-01-15 10:23:45 ERROR connection refused

Regex in substitutions

bash
# Capture groups: \1 \2 ... (BRE) or \1 with -E
sed 's/\(first\) \(last\)/\2 \1/'       file   # swap words (BRE)
sed -E 's/(first) (last)/\2 \1/'        file   # same, ERE
sed -E 's/([0-9]+)/[\1]/g'             file   # wrap numbers in []
sed 's/.*/  &/'                          file   # indent every line

# & represents the entire match
sed 's/[A-Z][a-z]*/[&]/g'  file   # wrap each capitalized word

# Alternate delimiters (useful for paths)
sed 's|/usr/local|/opt|g'   file
sed 's,/home/alice,/home/carol,g'  file

Output:

text
last first
last first
order [42] placed on [2026]-[01]-[15]
  Hello World
[The] [quick] [brown] [fox]
/opt/bin/tool
/home/carol/documents

Delete — d

d removes the addressed lines from the output and immediately moves to the next cycle — no further commands in the script run for the deleted line. Combine it with an address (line number, regex, or range) to strip headers, blank lines, or comment blocks.

bash
sed '/^$/d'            file   # delete empty lines
sed '/^\s*$/d'         file   # delete blank/whitespace-only lines
sed '/^#/d'            file   # delete comment lines
sed '1d'               file   # delete first line (skip header)

Output:

text
Hello World
foo bar
The quick brown fox
port=8080

p prints the current pattern space explicitly. It is almost always used with -n (which suppresses the default auto-print), so only lines selected by the address are printed — effectively making sed behave like a line-range or regex filter similar to grep.

bash
sed -n '5,10p'         file   # print lines 5–10 (like head/tail)
sed -n '/ERROR/p'      file   # print matching lines (like grep)
sed -n '$p'            file   # print last line
sed -n '1p'            file   # print first line

Output:

text
2026-01-15 10:23:45 ERROR connection refused
2026-01-15 10:45:01 ERROR timeout waiting for response

Insert, append, change — i, a, c

i inserts text before the addressed line, a appends text after it, and c replaces the entire line with new text. All three are useful for injecting config directives, banners, or boilerplate without a full substitution.

bash
sed '1i\# Header comment'    file   # insert before line 1
sed '$a\# Footer'            file   # append after last line
sed '/^Host /a\  StrictHostKeyChecking no'  ssh_config  # append after match

sed '5c\REPLACED LINE'       file   # change (replace) line 5
sed '/old text/c\new text'   file   # change matching line

Output:

text
# Header comment
Hello World
foo bar
The quick brown fox
# Footer

Read/write files — r, w

r splices the contents of a file into the output stream after the addressed line — handy for templating. w writes matching lines to a file instead of (or in addition to) stdout, letting you split a file into multiple outputs in a single pass.

bash
sed '/INSERT_HERE/r extra.txt'  template   # splice file contents
sed -n '/ERROR/w errors.txt'   app.log     # write matches to file

Output: (none — exits 0 on success)

Quit — q, Q

q prints the current line and exits immediately; Q exits without printing it. Both are more efficient than reading the whole file when you only need the beginning — sed '10q' is a lightweight substitute for head -10.

bash
sed '10q'   file   # print first 10 lines then quit (like head -10)
sed '/DONE/q' file # quit after first matching line
sed '0,/START/d; /END/q'  file  # print lines between START and END

Output:

text
line1
line2
line3
line4
line5
line6
line7
line8
line9
line10

In-place editing

-i rewrites the file on disk rather than printing to stdout — convenient but irreversible without a backup. Pass a suffix (e.g. .bak) to keep the original. BSD/macOS sed requires an explicit suffix even when you want no backup (-i ''), while GNU sed treats a bare -i as no backup.

bash
# GNU sed
sed -i 's/foo/bar/g' file.txt          # in-place, no backup
sed -i.bak 's/foo/bar/g' file.txt      # in-place with .bak backup

# macOS (BSD sed requires explicit suffix even for no-backup)
sed -i '' 's/foo/bar/g' file.txt

# Multiple files
sed -i 's/oldhost/newhost/g' *.conf
find . -name "*.py" -exec sed -i 's/python2/python3/g' {} +

Output: (none — exits 0 on success)

Multiple expressions

bash
sed -e 's/foo/bar/g' -e '/^#/d' -e 's/baz/qux/' file

# Or a semicolon-separated script
sed 's/foo/bar/g; /^#/d; s/baz/qux/' file

Output:

text
bar qux
Hello World
The quick brown bar

Hold space (advanced)

The hold space is a secondary buffer. Commands: h (copy pattern→hold), H (append), g (copy hold→pattern), G (append), x (exchange).

bash
# Reverse line order of a file
sed -n '1!G; h; $p' file

# Print the line before a match
sed -n '/error/{x;p;d}; h' file

# Delete duplicate consecutive lines (like uniq)
sed '$!N; /^\(.*\)\n\1$/!P; D' file

Output:

text
line3
line2
line1

Practical recipes

bash
# Remove trailing whitespace
sed 's/[[:space:]]*$//' file

# Trim leading whitespace
sed 's/^[[:space:]]*//' file

# Remove blank lines
sed '/^[[:space:]]*$/d' file

# Convert Windows CRLF to LF
sed 's/\r$//' file

# Extract lines between two patterns (exclusive)
sed -n '/BEGIN/{n; /END/!{p; b}; b}; /BEGIN/,/END/{/BEGIN/d; /END/d; p}' file

# Simpler exclusive range
sed -n '/START/,/END/{/START/d;/END/d;p}' file

# Number non-empty lines
sed '/./=' file | sed 'N; s/\n/ /'

# Double-space a file
sed 'G' file

# Extract value from key=value config
sed -n 's/^database_host=//p' config.ini

# Comment out lines matching a pattern
sed '/^ServerName/s/^/# /' httpd.conf

# Uncomment lines matching a pattern
sed '/^# ServerName/s/^# //' httpd.conf

Output:

text
Hello World
foo bar
The quick brown fox
1 Hello World
2 foo bar
3 The quick brown fox

Hello World

foo bar

The quick brown fox
db.internal.example.com
# ServerName www.example.com
ServerName www.example.com

Complete command reference

sed has more than a dozen single-letter commands beyond s/d/p. Most operate on the pattern space (the current line being processed) and a few interact with the hold space (a secondary buffer). Knowing the full alphabet is the difference between writing one-off substitutions and crafting maintainable sed scripts.

CmdDescription
sSubstitute
dDelete pattern space, start next cycle
DDelete up to first newline in pattern space, restart cycle
pPrint pattern space
PPrint up to first newline in pattern space
nReplace pattern space with next input line
NAppend next input line to pattern space (with \n)
gReplace pattern space with hold space
GAppend hold space to pattern space (with \n)
hReplace hold space with pattern space
HAppend pattern space to hold space (with \n)
xExchange pattern and hold spaces
i\Insert text before line
a\Append text after line
c\Change (replace) line(s)
r FILERead FILE, queue for output after this line
w FILEWrite pattern space to FILE
R FILEGNU: read one line from FILE per cycle
W FILEGNU: write up to first newline of pattern space to FILE
=Print line number
lList pattern space showing non-printable chars
qQuit (printing current line)
QQuit without printing
b LABELUnconditional branch to label
t LABELBranch to label IF a substitution succeeded since last input
T LABELGNU: branch IF NO substitution succeeded since last input
: LABELDefine a branch target
y/src/dst/Transliterate (one-for-one char map, like tr)
zGNU: zap (empty) pattern space
FGNU: print current filename
e [CMD]GNU: execute CMD (or pattern space) as shell command

Substitution flags in depth

The s command takes optional flags after the closing delimiter. They are independent and can be combined (gip, 2g, etc.). The numeric flag is special: it replaces only the N-th match on the line — combine with g to replace the N-th match and everything after it.

FlagMeaning
gReplace all matches on the line
N (1–9)Replace only the N-th match
NgReplace from N-th match onwards
i / ICase-insensitive (GNU)
pPrint the line if a substitution was made
w fileWrite the line to file if a substitution was made
eGNU: execute the result as a shell command and replace
m / MGNU: multi-line mode (when pattern space has \n)
bash
# Replace ONLY the 3rd occurrence
echo "a a a a a" | sed 's/a/X/3'

# Replace the 3rd and every later occurrence
echo "a a a a a" | sed 's/a/X/3g'

# Case-insensitive global replace, print only changed lines
sed -n 's/error/ERROR/Igp' app.log

# Substitute and write the changed lines to a separate file
sed -n 's/^FAIL/PASS/wp.log' results.txt

# `e` flag: substitute into a command, then execute it
echo "ls" | sed 's/.*/& -la/e'

Output (echo "a a a a a" | sed 's/a/X/3'):

text
a a X a a

Output (echo "a a a a a" | sed 's/a/X/3g'):

text
a a X X X

Backreferences and &

In a replacement string, & stands for the entire match and \1\9 reference the first nine capture groups (parenthesized subexpressions). Backreferences also work inside the regex itself to match a previously captured group — this is how you find repeated tokens. To get a literal & or \, escape it as \& or \\.

bash
# Wrap every IPv4 address in brackets
sed -E 's/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/[\1]/g' access.log

# Swap two columns separated by colons
sed -E 's/^([^:]+):([^:]+)$/\2:\1/' pairs.txt

# Quote every word starting with capital
sed -E 's/\b([A-Z][a-z]+)\b/"\1"/g' file

# Use & to repeat the match (here: duplicate every digit)
echo "abc 12 xyz 9" | sed -E 's/[0-9]+/& &/g'

# Backreference inside the pattern (find repeated words)
sed -nE 's/\b(\w+) \1\b/MATCH: &/p' essay.txt

# Literal & and \ in replacement
echo "AT" | sed 's/AT/Tom \& Jerry/'   # & is reserved
echo "AT" | sed 's/AT/back\\slash/'    # \\ -> \

Output (echo "abc 12 xyz 9" | sed -E 's/[0-9]+/& &/g'):

text
abc 12 12 xyz 9 9

Addresses in depth

Addresses select the lines a command applies to. They can be line numbers, regexes, ranges, or a special form. Suffixing ! negates the address. GNU sed adds the 0,/re/ form, which is critical when the first match might occur on line 1 (the standard 1,/re/ would never terminate the range in that case).

bash
# Line-number addresses
sed '3,7d'      file   # delete lines 3 through 7
sed '$!d'       file   # delete every line except the last
sed '5,$d'      file   # delete from line 5 to EOF
sed '0~3p' -n   file   # every 3rd line (0,3,6,9,…) — GNU
sed '1~2d'      file   # delete odd lines — GNU

# Regex addresses
sed '/^#/d'     file   # delete comment lines
sed '/^$/,/^$/d' file  # delete every block ending in blank
sed '/^DEBUG/!d' file  # KEEP only DEBUG lines (delete others)

# Range from regex to regex
sed -n '/BEGIN/,/END/p' file       # inclusive
sed     '/BEGIN/,/END/d' file       # delete inclusive range

# GNU 0,/re/ form — handles match on line 1
sed '0,/foo/{s/foo/bar/}' file      # replace only first occurrence (anywhere)
sed '1,/foo/{s/foo/bar/}' file      # WRONG if foo is on line 1

Output (sed -n '/BEGIN/,/END/p' file):

text
BEGIN
intro
body
END

GNU sed vs BSD/macOS sed

The two implementations diverge in three places that bite people daily: in-place editing (-i), extended-regex flag (-E vs -r), and the GNU-only i\/a\/c\ shortcuts. macOS ships BSD sed by default; brew install gnu-sed provides gsed as a workaround. The cleanest cross-platform strategy is to avoid -i entirely in shared scripts.

BehaviourGNU sedBSD/macOS sed
In-place no backupsed -i or sed -i ''sed -i '' (suffix required)
In-place with backupsed -i.baksed -i.bak
Extended regex-E or -r-E only
i\TEXT on one lineworksrequires literal newline
0,/re/ addresssupportednot supported (use /re/ workaround)
\b word boundarysupportednot in BRE
Case-insensitive s///isupportedsupported
T (negated t)supportednot supported
bash
# Portable in-place pattern (no -i)
tmp=$(mktemp) && sed 's/foo/bar/g' file > "$tmp" && mv "$tmp" file

# Portable extended regex flag
sed -E 's/(foo|bar)/X/g' file   # both implementations

# Portable insert: feed sed a real newline
sed '1i\
# Header line
' file

# Portable replace-first-match (avoids 0,/re/ on BSD)
sed -e ':a' -e '/foo/{s//bar/; bb}' -e 'n; ba' -e ':b' file

Output: (none — exits 0 on success)

Pattern space and hold space — the mental model

sed processes input in cycles: read one line into the pattern space, run the entire script top to bottom, then (unless -n) print the pattern space and start the next cycle. The hold space is a separate buffer that survives between cycles, accessed only by h, H, g, G, and x. Almost every "magic" sed one-liner is hold-space gymnastics.

text
+-------------------+        +------------------+
| input  line 1     | -----> | pattern space    |
+-------------------+   read +------------------+
                                ^   ^   ^   ^
                                |   |   |   |   commands run here
                                v   v   v   v
                            +------------------+
                            | hold space       |   (h H g G x)
                            +------------------+
                                |
                                v   auto-print (unless -n)
                            stdout
bash
# Show every two consecutive lines together (line N and N+1)
sed 'N; s/\n/ | /' file

# Tac (reverse line order) using hold-space accumulation
sed -n '1!G; h; $p' file

# Print line N along with the preceding line (here N=3)
sed -n '2{h;n;d}; 3{x;p;x;p}' file

# Keep the LAST occurrence of a duplicate (opposite of `uniq`)
sed -n 'G; /^\([^\n]*\)\n.*\1.*/!P; h' file

Output: (none — exits 0 on success)

Branching with labels, b, t, T

Labels (:name) act like targets for b (unconditional branch), t (branch if last s/// succeeded), and T (branch if it did not — GNU only). With branches, sed becomes Turing-complete; in practice they are essential for "keep substituting until no more changes" loops and multi-line aggregations.

bash
# Squeeze runs of blank lines to a single blank line
sed -e '/./,/^$/!d' file

# Repeatedly substitute until no further match (multi-pass on one line)
sed ':loop
s/  / /g
t loop' file

# Join lines ending in backslash (line continuation)
sed -e ':start' -e '/\\$/{N; s/\\\n//; b start}' file

# Replace only OUTSIDE a region — skip lines between BEGIN…END
sed '/BEGIN/,/END/b skip
s/foo/bar/g
:skip' file

Output (sed ':loop; s/ / /g; t loop' "x y z"):

text
x y z

r, w, R, W — file I/O

r FILE and w FILE are sed's I/O primitives. r queues the contents of a file to be printed after the current pattern space; w writes pattern space to a file. GNU R and W are line-at-a-time variants useful for interleaving inputs.

bash
# Append a footer file after every match of a marker
sed '/MARKER/r footer.txt' template > out

# Split a log into two files by severity
sed -n '/ERROR/w errors.log
/WARN/w warns.log' app.log

# Interleave two files line-by-line (zip)
sed 'R file2' file1

Output (sed '/MARKER/r footer.txt' template):

text
intro line
MARKER
--- footer line 1 ---
--- footer line 2 ---
body line

Delimiters in s///

The / separator in s/pat/repl/ can be any single character that does not appear in the pattern or replacement. For paths, URLs, or anything containing slashes, switch to |, #, ,, or @ — this avoids tedious escaping like s/\/usr\/local\/bin/\/opt\/bin/. The delimiter must be the same character three times.

bash
sed 's|/usr/local|/opt|g'        paths.txt
sed 's#https://#http://#g'       urls.txt
sed 's,/home/alice,/home/carol,g'  paths.txt
sed -E 's@\.com/(api|auth)@.net/\1@g' endpoints.txt

Output: (none — exits 0 on success)

Transliteration with y///

y/SRC/DST/ is sed's mini-tr: every character in SRC maps to the corresponding character in DST, one-for-one. Useful for case conversion of a fixed alphabet or simple ciphers. Unlike s, y does not understand regex.

bash
# Uppercase every letter on lines matching /^name:/
sed '/^name:/y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' file

# ROT-13
sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm/' file

# Swap tabs for spaces (single-character)
sed 'y/\t/ /' file

Output: (none — exits 0 on success)

sed vs awk vs perl -pe

These three tools occupy the same "stream editor" niche; choosing between them is mostly about regex needs and complexity. sed wins on size and portability, awk on field-oriented data, perl on regex power and one-off scripts.

NeedReach for
Simple line-by-line substitutionsed s///
In-place edits across many filessed -i or perl -i -pe
Field-aware logic ($3 > 100)awk
Aggregation, sums, countsawk
Modern regex (\d, lookarounds, non-greedy)perl -pe
Multi-line patternsperl -0pe or sed -z
JSON / structured datajq (not sed)
CSV with quoted commasawk -v FPAT=... or csvkit
bash
# Same task: replace foo with bar on every line, three ways
sed     's/foo/bar/g'  file
awk     '{gsub(/foo/, "bar"); print}' file
perl -pe 's/foo/bar/g' file

# Modern regex with lookbehind — sed cannot do this; perl can
perl -pe 's/(?<=Bearer )\S+/REDACTED/' headers.txt

# Slurp whole file as one record — perl idiom, hard in sed
perl -0pe 's/<form.*?<\/form>/<form>REDACTED<\/form>/gs' page.html

Output: (none — exits 0 on success)

Modern alternative — sd

sd (current release v1.1.0, Feb 2026) is a Rust find-and-replace tool whose pitch is "sed with the s/// dialect everyone actually remembers". Patterns use the same regex flavour as JavaScript / Python (\d, \w, named groups, non-greedy *?), and the replacement syntax is $1 / ${name} instead of \1. It is purpose-built for the one job that 90% of sed invocations actually do — global substitution — so it skips addressing, hold space, branching, and -n. For everything else, you still want sed.

bash
# Install
cargo install sd                # or: brew install sd

# Stdin pipe — no need for / delimiters
echo "hello world" | sd 'world' 'rust'

# File in place (atomic write — safe to interrupt, unlike sed -i)
sd 'old_api' 'new_api' src/**/*.py

# Preview before writing (no -i flag needed; -p shows the diff)
sd -p 'foo' 'bar' file.txt

# Literal/string mode — no escaping of regex metacharacters
sd -F 'a.b.c' 'x-y-z' config.ini

# Modern regex — lookarounds, non-greedy, \d, named groups
sd '(?<year>\d{4})-(?<mon>\d{2})' '${mon}/${year}' dates.txt

# Across-line matching (multi-line / DOTALL semantics)
sd -A '<form>.*?</form>' '<form>REDACTED</form>' page.html

Output (echo "hello world" | sd 'world' 'rust'):

text
hello rust
Capabilitysedsd
POSIX BRE / EREYesNo (Rust regex only)
Lookarounds, \d, non-greedyNoYes
Address ranges, hold space, branchingYesNo
Atomic in-place writesNo (-i is not atomic)Yes
Preview without writingNoYes (-p)
Literal-string modeNo (must escape)Yes (-F)
Speed on large regex jobsBaseline~11× faster (reported)
Drop-in for existing sed scriptsYesNo

Reach for sd for interactive search-and-replace at the shell, especially when the pattern has slashes, escapes, or lookarounds. Keep sed for scripts, pipelines you ship to other people, and anything that needs -i.bak, hold space, or address ranges. They coexist — sd does not replace sed in CI or POSIX environments.

Pitfalls and gotchas

  • Greedy quantifiers: .* is greedy. s/<.*>/X/ on <a><b> becomes X, not X<b>. Use [^>]* for "anything up to the next >" or switch to perl -pe for non-greedy .*?.
  • Special chars in delimiter: s/a/b/ breaks if a contains a literal /. Switch delimiter (s|a|b|) — much easier than escaping every slash.
  • Backslash in replacement: &, \1\9, and \\ are special in the replacement string. To insert a literal \, write \\.
  • BSD vs GNU -i: macOS sed treats -i as "use this suffix for backup", so sed -i 's/x/y/' file consumes s/x/y/ as the backup suffix. Always pass '' on BSD: sed -i '' 's/x/y/' file.
  • Newlines in replacement: BRE/ERE replacements cannot contain a literal newline directly. Use \n only with GNU sed; portable form is to end the line with \ and continue on the next.
  • Shell quoting: single-quote sed scripts. Inside double quotes the shell eats \, $, and backticks before sed sees them.
  • d and D differ: d deletes the entire pattern space and starts the next cycle. D deletes only up to the first embedded newline (relevant when you have used N to append a second line).
  • n and N differ: lowercase n replaces pattern space with the next line. Uppercase N appends the next line with an embedded \n.
  • -i is not transactional: a failing sed mid-stream leaves the file partially written. Use a mktemp/mv pattern for anything irreplaceable.

Workflow recipes

bash
# 1. Batch-rename a key in every YAML file under a tree
find . -name '*.yaml' -print0 | xargs -0 sed -i 's/^oldKey:/newKey:/'

# 2. Extract the section between two markers (exclusive of markers)
sed -n '/^## Installation/,/^## /{/^## /d; p}' README.md

# 3. Extract the value of a key from a key=value config
sed -n 's/^[[:space:]]*database_host[[:space:]]*=[[:space:]]*//p' app.ini

# 4. Normalize line endings (CRLF → LF, and trailing whitespace away)
sed -i 's/\r$//; s/[[:space:]]*$//' file.txt

# 5. Indent every line by 4 spaces
sed 's/^/    /' file

# 6. Strip ANSI color escape codes from a log
sed -E 's/\x1B\[[0-9;]*[mK]//g' colored.log

# 7. Add a comment on the first occurrence of a directive
sed '0,/^Listen 80$/{s//# Default HTTP port\nListen 80/}' httpd.conf

# 8. Print every paragraph (blank-line-separated record) that mentions FOO
sed -n '/./{H; $!d}; x; /FOO/p' file

# 9. Replace text matching $OLD with $NEW, escaping shell variables safely
old='alice@example.com'
new='carol@example.com'
# Escape regex/replacement meta-characters for sed
esc_old=$(printf '%s' "$old" | sed -e 's/[]\/$*.^[]/\\&/g')
esc_new=$(printf '%s' "$new" | sed -e 's/[\/&]/\\&/g')
sed -i "s/${esc_old}/${esc_new}/g" addresses.txt

# 10. Find-then-edit with rg (preview first, then commit)
rg -l 'old_api' src/ | xargs sed -i 's|old_api|new_api|g'

Output (sed -n '/^## Installation/,/^## /{/^## /d; p}' README.md):

text
Install via your platform package manager:

    apt install foo
    brew install foo

After installation, verify with `foo --version`.

One-liners reference

bash
sed -n '$=' file                  # count lines (wc -l)
sed -n '5p' file                  # print line 5
sed -n '$p' file                  # print last line
sed -n '/re/p' file               # grep equivalent
sed -n '/re/!p' file              # grep -v equivalent
sed '/./,$!d' file                # remove leading blank lines
sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' file   # remove trailing blank lines
sed = file | sed 'N;s/\n/\t/'     # number every line (cat -n)
sed '$!N;s/\n/ /'  file           # join every pair of lines
sed 'G' file                      # double-space
sed -n 'p;N' file                 # double-space (alt)
sed 'n;d' file                    # print only odd lines
sed '1~2d' file                   # print only even lines (GNU)
sed '/^$/N;/\n$/D' file           # squeeze multiple blanks to one
sed 's/.$//' file                 # delete last char of each line
sed 's/^.//' file                 # delete first char of each line
sed '1!G;h;$!d' file              # reverse lines (tac)

Output: (none — exits 0 on success)

For complex, multi-field transformations, awk is usually clearer than sed. Use sed for line-oriented substitutions and deletions; switch to awk when you need to reference fields or do arithmetic.

When a sed script grows past a few commands, move it into a file (script.sed) and run sed -f script.sed. You can add # comments and break logic across lines, which makes branching scripts much easier to read.

Pair rg -l (or grep -rl) with sed -i for a fast "find-then-replace" pipeline across a tree. Preview the file list first, then run the substitution.

[!WARN] The in-place behaviour differs between GNU sed (-i suffix optional) and BSD/macOS sed (-i '' required). For portable scripts, write to a temp file and mv instead of using -i.

[!WARN] sed -i is not atomic — an interrupted run can leave a file truncated. For anything irreplaceable, use a mktemp/mv pattern or work on a copy.

Sources