sed — Stream Editor Guide for Text Manipulation
sed — the stream editor — is one of the oldest Unix tools still in daily use. It reads input line by line, applies editing commands, and writes the result to stdout. It excels at substitution and transformation tasks where you’d otherwise reach for a full text editor or a throw-away Python script.
Basic Syntax
sed 'command' file
Or pipe into it:
$ echo "Hello World" | sed 's/World/Unix/'
Hello Unix
Multiple commands can be chained with -e or separated by semicolons:
$ echo "foo bar baz" | sed -e 's/foo/FOO/' -e 's/bar/BAR/'
FOO BAR baz
Substitution: s/pattern/replacement/flags
Substitution is the most-used sed command. The basic form replaces the first match on each line:
$ echo "cat and cat" | sed 's/cat/dog/'
dog and cat
Add the g flag to replace all occurrences on each line:
$ echo "cat and cat" | sed 's/cat/dog/g'
dog and dog
Case-insensitive matching
GNU sed (Linux) supports I:
$ echo "Cat and CAT" | sed 's/cat/dog/gI'
dog and dog
macOS ships with BSD sed, which doesn’t support I — install GNU sed with brew install gnu-sed and use it as gsed.
Backreferences
Capture groups with \( \) and reference them with \1, \2:
$ echo "2024-01-15" | sed 's/\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)/\3\/\2\/\1/'
15/01/2024
With extended regex (-E on macOS, -r on GNU sed), parentheses don’t need escaping:
$ echo "2024-01-15" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
15/01/2024
Using a different delimiter
When your pattern or replacement contains /, swap the delimiter for anything else:
$ echo "/usr/local/bin" | sed 's|/usr/local|/opt|'
/opt/bin
Addresses: Targeting Specific Lines
By default a command runs on every line. Prefix it with an address to restrict it.
Line number:
$ sed '3s/foo/bar/' file.txt
Only substitutes on line 3.
Last line ($):
$ sed '$d' file.txt
Deletes the last line.
Regex address:
$ sed '/^#/d' config.txt
Deletes all comment lines (lines starting with #).
Range of lines (start,end):
$ sed '5,10s/old/new/' file.txt
Applies the substitution to lines 5 through 10 only.
Range with patterns:
$ sed '/START/,/END/d' file.txt
Deletes everything from the line matching START to the line matching END (inclusive).
Every Nth line (first~step) (GNU sed only):
$ sed -n '1~2p' file.txt
Prints every odd-numbered line.
Deletion: d
$ sed '/^$/d' file.txt
Removes blank lines.
$ sed '1d' file.txt
Removes the first line (handy for stripping a CSV header).
Printing: p and -n
By default sed prints every line. Use -n to suppress automatic printing, then p to print only what you want:
$ sed -n '/ERROR/p' app.log
2024-01-15 ERROR connection refused
2024-01-15 ERROR disk full
Equivalent to grep ERROR app.log, but you can combine it with substitution:
$ sed -n 's/ERROR/CRITICAL/p' app.log
2024-01-15 CRITICAL connection refused
Inserting and Appending Lines
Insert a line before a match (i):
$ sed '/^server {/i # Auto-generated config' nginx.conf
Append a line after a match (a):
$ sed '/listen 80;/a\ listen [::]:80;' nginx.conf
In-Place Editing with -i
-i edits the file in place. Always make a backup first with an extension:
$ sed -i.bak 's/localhost/db.prod.internal/g' config.yaml
This writes the original to config.yaml.bak and updates config.yaml. On macOS (BSD sed), the extension is mandatory. On GNU sed, you can use -i without an extension.
To edit with no backup on GNU sed:
$ sed -i 's/v1/v2/g' api_config.json
Multiline Patterns with N and P
N reads the next line into the pattern space. This lets you match across line boundaries:
$ printf "foo\nbar\nbaz\n" | sed 'N;s/foo\nbar/replaced/'
replaced
baz
Practical Real-World Examples
Strip trailing whitespace from every line:
$ sed 's/[[:space:]]*$//' file.txt
Comment out lines containing a keyword:
$ sed 's/^\(.*debug.*\)$/#\1/' config.py
Extract lines between two markers (exclusive):
$ sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' server.pem
Double-space a file (add a blank line after each):
$ sed 'G' file.txt
Number lines (without nl):
$ sed '=' file.txt | sed 'N;s/\n/\t/'
1 first line
2 second line
Remove HTML tags:
$ echo "<p>Hello <b>World</b></p>" | sed 's/<[^>]*>//g'
Hello World
Replace the Nth occurrence on a line (e.g. the 2nd):
$ echo "a a a" | sed 's/a/b/2'
a b a
sed vs awk vs grep — Quick Decision Guide
| Task | Reach for |
|---|---|
| Find matching lines | grep |
| Substitution / deletion | sed |
| Field extraction, arithmetic, reports | awk |
| Multi-column transforms | awk |
Conclusion
sed is at its best for substitution, deletion, and line-range targeting — especially in pipelines and shell scripts where you need a quick transformation without standing up a full script. The combination of address ranges and the s command handles the vast majority of real-world use cases. When your transformations start needing field-awareness or arithmetic, that’s the signal to hand off to awk or a scripting language.