sed
Edit text streams with a single pass, line by line
sed (stream editor) reads input one line at a time, applies a script of editing
commands to each line, and prints the result. Where grep only decides
which lines to show, sed decides how to transform them: substitute text, delete lines,
insert new ones, all without opening an editor or writing a program.
The mental model: the pattern space and the cycle
For each input line, sed copies it into a working buffer called the pattern space, runs
your whole script against that buffer, then (unless told otherwise) prints the pattern space and
moves to the next line. This read-script-print loop is the "cycle." Almost every confusing sed
behaviour makes sense once you remember every command in your script runs once per line, not
once for the whole file. s/foo/bar/ inside a script isn't "replace foo with bar in the file,"
it's "replace foo with bar in whatever line I'm looking at right now."
Substitution: s/pattern/replacement/flags
The workhorse command, and the one most people mean when they say "sed":
sed "s/ERROR/CRITICAL/" app.log # first match per line
sed "s/ERROR/CRITICAL/g" app.log # every match per line (g = global)
Without g, only the first match on each line is replaced, a constant source of "why didn't
this replace everything" confusion. The / delimiters are conventional, not mandatory: if your
pattern or replacement contains a lot of literal slashes (paths are the classic case), pick a
different delimiter. s#/etc/app#/opt/app# reads far better than escaping every /.
Addressing: deciding which lines a command applies to
Every command can be prefixed with an address restricting it to specific lines:
sed -n "2p" file # just line 2
sed -n "2,4p" file # lines 2 through 4
sed -n "/ERROR/p" file # lines matching a regex
sed "/DEBUG/d" file # delete lines matching a regex
sed "1!d" file # everything EXCEPT line 1 ('!' negates)
-n suppresses the default auto-print, which is why it pairs so naturally with p. Without
-n, sed -n "2p" would print line 2 twice (once from p, once from the automatic print).
GNU sed also supports a first~step address (1~2 = every odd line) that POSIX sed doesn't
have.
Basic regular expressions by default: same trap as grep
Plain sed uses BRE, where +, ?, |, and () need backslash-escaping to mean anything
special. -E (or -r on some systems) switches to ERE, letting you write
s/([a-z]+),([0-9]+)/\2:\1/ instead of the backslash-heavy BRE equivalent. Same tradeoff as
grep: reach for -E the moment your pattern needs grouping or alternation.
Editing files in place
-i rewrites the file directly instead of printing to stdout. -i.bak does the same thing but
keeps a backup of the original with .bak appended to the name first.
Beyond one line: the hold space
sed also has a second buffer, the hold space, that persists across cycles. Commands like
h (copy pattern space to hold space), H (append instead of copy), g/G (copy back the
other way), and x (swap the two) let a script remember something from an earlier line and use
it later (reversing a file, joining consecutive lines, printing a line before a match). It's
a small toolkit, but it's the reason sed can do more than pure per-line substitution. Reach
for it when a single-pass awk script starts feeling more natural than a sed one-liner, since
that's usually the sign the hold space is what you actually need.
Chaining multiple commands
Separate commands with ;, repeat -e once per command, or put them one per line in a script
file loaded with -f script.sed. All three are equivalent. -e/-f matter mainly when a
command itself contains a ; your shell would otherwise interpret.
Basic substitution
The s/// command, first match vs every match.
Replace the first match on each line
sed "s/ERROR/CRITICAL/" app.log
Without the `g` flag, only the first match per line is replaced. Here there's only one per line anyway.
Show output
INFO: service starting
DEBUG: loading config from /etc/app.conf
INFO: listening on port 8080
WARN: deprecated option 'foo' used
CRITICAL: connection refused
INFO: retrying connection
CRITICAL: connection refused
INFO: connected successfully
Replace every match on each line
sed "s/connection refused/conn refused/g" app.log
`g` (global) replaces every match on a line, not just the first.
Show output
INFO: service starting
DEBUG: loading config from /etc/app.conf
INFO: listening on port 8080
WARN: deprecated option 'foo' used
ERROR: conn refused
INFO: retrying connection
ERROR: conn refused
INFO: connected successfully
Replace only the Nth match on a line
echo "foo bar foo baz foo qux" | sed "s/foo/FOO/2"
A number instead of (or before) `g` replaces just that occurrence, here the 2nd.
Show output
foo bar FOO baz foo qux
Replace the Nth match and every one after it
echo "foo bar foo baz foo qux" | sed "s/foo/FOO/2g"
Combining a number with `g` replaces that occurrence and all later ones on the line, leaving earlier ones untouched.
Show output
foo bar FOO baz FOO qux
Use a different delimiter to avoid escaping slashes
sed "s#/etc/app#/opt/app#" config.conf
Any character can be the delimiter, not just `/`. Pick one that doesn't appear in your pattern or replacement to skip a wall of backslashes.
Add text to the start of every line
sed "s/^/ /" config.conf
An empty match at `^` (start of line) still counts as a match. This is the standard indent-every-line idiom.
Show output
# Server configuration
host=localhost
port=8080
debug=false
timeout=30
Transliterate characters one-for-one
echo hello | sed "y/el/ip/"
`y/set1/set2/` maps each character in set1 to the character in the same position in set2. Unlike `s///`, it's not regex, just direct character substitution.
Show output
hippo
Selecting lines to print
sed -n plus p, the read-only counterpart to grep.
Print a specific line number
sed -n "2p" app.log
`-n` suppresses sed's normal auto-print of every line; `2p` then explicitly prints just line 2.
Show output
DEBUG: loading config from /etc/app.conf
Print a range of line numbers
sed -n "2,4p" app.log
A comma joins two addresses into an inclusive range.
Show output
DEBUG: loading config from /etc/app.conf
INFO: listening on port 8080
WARN: deprecated option 'foo' used
Print lines matching a pattern
sed -n "/ERROR/p" app.log
A `/regex/` address selects lines the same way grep would. sed just happens to be able to do more to them afterward.
Show output
ERROR: connection refused
ERROR: connection refused
Print everything except a specific line
sed -n "1!p" config.conf
`!` negates the address. This prints every line except line 1.
Show output
host=localhost
port=8080
debug=false
timeout=30
Print the last line
sed -n '$p' config.conf
`$` addresses the last line, whatever its number. Useful when you don't know a file's length in advance.
Show output
timeout=30
Print every other line
printf "a\nb\nc\nd\ne\n" | sed -n "1~2p"
`first~step` is a GNU extension: starting at line 1, print every 2nd line. Not available in POSIX/BSD sed.
Show output
a
c
e
Print a range between two patterns
sed -n "/WARN/,/ERROR/p" app.log
A range doesn't need line numbers on either end. Here it prints from the first WARN line through the next ERROR line.
Show output
WARN: deprecated option 'foo' used
ERROR: connection refused
Deleting lines
The d command, and its addressing in common with p.
Delete lines matching a pattern
sed "/DEBUG/d" app.log
`d` deletes the addressed lines from the output instead of printing them. No `-n` needed since deleted lines simply don't reach the auto-print step.
Show output
INFO: service starting
INFO: listening on port 8080
WARN: deprecated option 'foo' used
ERROR: connection refused
INFO: retrying connection
ERROR: connection refused
INFO: connected successfully
Delete the first line
sed "1d" app.log
A bare line number address, same as with `p`.
Delete the last line
sed '$d' app.log
A common use: stripping a trailing blank line or footer that's always at the end.
Delete comment lines
sed "/^#/d" config.conf
Strip lines starting with `#`, the sed equivalent of `grep -v '^#'`, useful when you're already editing the file with sed for other reasons.
Show output
host=localhost
port=8080
debug=false
timeout=30
Editing files in place
-i rewrites the file directly, optionally keeping a backup.
Edit a file in place, keeping a backup
sed -i.bak "s/8080/9090/" config.conf
`-i.bak` writes the change directly to `config.conf` and saves the pre-edit version as `config.conf.bak`. Always do this before `-i` with no suffix, until you fully trust the script.
Show output
# Server configuration
host=localhost
port=9090
debug=false
timeout=30
Edit a file in place with no backup
sed -i "s/8080/9090/" config.conf
Same edit, no safety net. Reach for this only once you've verified the script's behaviour, ideally on a copy first.
Inserting, appending, and changing lines
a, i, and c add or replace whole lines instead of editing text within one.
Insert a new line after a match
sed '2a\newline_after' config.conf
`a\text` (append) adds a new line containing `text` immediately after the addressed line.
Show output
# Server configuration
host=localhost
newline_after
port=8080
debug=false
timeout=30
Insert a new line before a match
sed '2i\newline_before' config.conf
`i\text` (insert) is `a`'s mirror image. The new line goes before the addressed line instead of after.
Show output
# Server configuration
newline_before
host=localhost
port=8080
debug=false
timeout=30
Replace a whole line
sed '2c\replaced_line' config.conf
`c\text` (change) replaces the entire addressed line with `text`, rather than editing part of it like `s///` would.
Show output
# Server configuration
replaced_line
port=8080
debug=false
timeout=30
Regex groups and extended syntax
Capturing parts of a match and reusing them in the replacement.
Reference the whole match in the replacement
sed "s/[0-9]\+/NUM/g" names.csv
No capture groups needed here, just a straight replacement of every run of digits.
Show output
alice,NUM,engineer
bob,NUM,designer
carol,NUM,manager
Reorder fields using capture groups
sed -E "s/([a-z]+),([0-9]+)/\2:\1/" names.csv
`-E` enables ERE so `()` groups without escaping; `\1`/`\2` in the replacement refer back to what each group matched.
Show output
30:alice,engineer
25:bob,designer
35:carol,manager
Replace every delimiter in a line
sed "s/,/ | /g" names.csv
A simple literal substitution used as a quick CSV-to-readable-table converter.
Show output
alice | 30 | engineer
bob | 25 | designer
carol | 35 | manager
Multiple commands in one script
Three equivalent ways to chain commands.
Chain commands with a semicolon
sed "s/localhost/127.0.0.1/;s/8080/9090/" config.conf
Semicolons separate commands within a single `-e`/script argument, run in order against each line.
Show output
# Server configuration
host=127.0.0.1
port=9090
debug=false
timeout=30
Chain commands with multiple -e flags
sed -e "s/localhost/127.0.0.1/" -e "s/8080/9090/" config.conf
Equivalent to the semicolon version. Clearer when a command itself contains a semicolon your shell would otherwise need escaped.
Show output
# Server configuration
host=127.0.0.1
port=9090
debug=false
timeout=30
Load a script from a file
printf 's/localhost/127.0.0.1/\ns/8080/9090/\n' > script.sed
sed -f script.sed config.conf
`-f` reads one command per line from a file. Worth it once a script grows past a couple of commands or gets reused across files.
Show output
# Server configuration
host=127.0.0.1
port=9090
debug=false
timeout=30
Practical patterns
Common tasks that combine the commands above.
Stop processing after a given line
sed "5q" app.log
`q` quits immediately once the addressed line is reached, like `head`, but as part of a larger sed script instead of a separate command.
Show output
INFO: service starting
DEBUG: loading config from /etc/app.conf
INFO: listening on port 8080
WARN: deprecated option 'foo' used
ERROR: connection refused