sed

Edit text streams with a single pass, line by line

Updated 2026-07-05

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

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 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 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