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 decides which lines
to show, sed changes them: substitute text, delete lines, insert new ones, without opening an
editor or writing a program.
Why every command runs once per line
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. That read-script-print loop is the cycle.
Every command in your script therefore runs once per line. s/foo/bar/ does not mean "replace
foo with bar in the file". It means "replace foo with bar in the line currently in the pattern
space".
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, which accounts for most of the "why
didn't that replace everything" questions. The / delimiters are conventional rather than
mandatory: if the pattern or replacement is full of literal slashes, pick a different one.
s#/etc/app#/opt/app# reads far better than escaping every /.
Addressing: 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 with p. Without it, sed "2p"
prints line 2 twice: once from p, once from the automatic print. GNU sed also supports a
first~step address (1~2 is every odd line) that POSIX sed doesn't have.
BRE by default, ERE with -E
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: use -E the moment your pattern needs grouping or alternation.
Rewriting a file instead of printing
-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.
To see what an edit would change rather than reading the whole file, pipe the result into
diff against the original: sed "s/ERROR/CRITICAL/g" app.log | diff app.log -
Across a whole tree rather than one file, -i is driven by a list of filenames from somewhere
else; find and replace across many files is that
pipeline, including what to do about a filename with a space in it.
prints only the lines that differ, and prints nothing at all when the script matched nothing.
The hold space: carrying data between lines
sed has a second buffer, the hold space, which persists across cycles. h copies the pattern
space into it, H appends instead, g and G copy back the other way, and x swaps the two.
That is enough to reverse a file, join consecutive lines, or print the line before a match.
When a script starts wanting state that survives from one line to the next, the hold space is
where sed keeps it. An awk script is often the better tool at that point.
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.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
app.log 8 log lines with INFO / DEBUG / WARN / ERROR levels
INFO: service starting
DEBUG: loading config from /etc/app.conf
INFO: listening on port 8080
WARN: deprecated option 'foo' used
ERROR: connection refused
INFO: retrying connection
ERROR: connection refused
INFO: connected successfully
config.conf a comment line plus four key=value settings
# Server configuration
host=localhost
port=8080
debug=false
timeout=30
names.csv three rows of name,age,role - no header
alice,30,engineer
bob,25,designer
carol,35,manager
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 && cat 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. Use it only once you have checked 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
Commands can be chained in several equivalent ways.
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