opscape

Text processing

sed

Edit a stream of text with a script of commands, most often to substitute text or delete lines.

sed [OPTION...] SCRIPT [FILE...]

15 of 15 examples run in Debian 13.7 on 2026-10-31 · sed 4.9-2+deb13u1 · manual: sed(1)

sed is a stream editor: it reads input one line at a time into a buffer (the "pattern space"), runs your script against it, prints the result, and moves on. It never holds the whole file in memory and, unless you pass -i, never changes the file; the edited text goes to standard output.

Almost every real use is one of a handful of shapes: s/old/new/ to substitute, /regex/d to delete lines, -n '/regex/p' to print only some lines, and an address in front of a command to limit it to a line number, a range, or the lines matching a pattern. Learn those and you have 95% of sed.

Patterns are basic regular expressions by default, where (, ), +, ? and | are literal; -E gives you extended syntax so groups and + work without backslashes. Reach for awk instead when the edit depends on fields or arithmetic, and for grep when you only need to find lines.

Examples

Replace the first match on each line

sed 's/fix/FIX/' todo.txt
explain
output
buy milk
FIX login bug
write tests
FIX logout bug
deploy v2.4

s/old/new/ replaces the first occurrence of fix on each line. Lines without a match pass through unchanged. The file itself is not modified; the result goes to the terminal.

Replace every match on the line

echo 'a-b-c-d' | sed 's/-/:/'
echo 'a-b-c-d' | sed 's/-/:/g'
echo 'a-b-c-d' | sed 's/-/:/2'
output
a:b-c-d
a:b:c:d
a-b:c-d

Without a flag only the first - changes. The g flag replaces all of them, and a number replaces only that occurrence: 2 changes the second dash.

Use another delimiter for paths

sed 's|/var/log/demo|/srv/logs|' data/settings.conf
explain
output
# demo service settings
host = 0.0.0.0
port = 8080

# logging
log_level = info
log_file = /srv/logs/app.log
# max_connections = 100
timeout = 30

The character after s becomes the delimiter, so paths full of slashes need no escaping. |, # and , are common choices. Only the log_file line contains the path.

Edit a file in place and keep a backup

sed -i.bak 's/^port = .*/port = 9090/' data/settings.conf
grep -H '^port' data/settings.conf data/settings.conf.bak
output
data/settings.conf:port = 9090
data/settings.conf.bak:port = 8080

-i.bak rewrites the file and first saves the original as settings.conf.bak. The grep shows the new value in the file and the old one in the backup. -i with no suffix edits without a backup; there is no undo.

Delete comments and blank lines

sed -e '/^#/d' -e '/^$/d' data/settings.conf
explain
output
host = 0.0.0.0
port = 8080
log_level = info
log_file = /var/log/demo/app.log
timeout = 30

/regex/d deletes every line the address matches. Two -e options run as one script, in order. The commented-out # max_connections line goes too, since it starts with #.

Print only some lines with -n and p

sed -n '/WARN/p' logs/app.log
explain
output
2026-03-14 08:03:45 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:03:46 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:11:47 WARN  permission denied: DELETE /api/users/3
2026-03-14 08:20:12 WARN  blocked request for /.env from 198.51.100.23

-n turns off sed's automatic printing, and p prints the lines the address selects. This is equivalent to grep WARN. Forgetting -n prints every line once and the matching lines twice.

Print a range of lines by number

sed -n '3,5p' logs/app.log
explain
output
2026-03-14 08:03:45 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:03:46 WARN  failed login for user=alice from 198.51.100.7
2026-03-14 08:05:09 INFO  user lookup id=7 not found

An address can be a line number or a range first,last. This prints lines 3 to 5. $ means the last line, so sed -n '10,$p' prints from line 10 to the end.

Operate between two patterns

sed -n '/health check failed/,/recovered/p' logs/app.log
explain
output
2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms
2026-03-14 08:16:50 INFO  health check recovered

A range can start and end on patterns. sed starts printing at the first line matching health check failed and stops after the next line matching recovered, inclusive. If the end pattern never matches, the range runs to the end of the file.

Substitute only on matching lines

sed '/^log_/s/ = / := /' data/settings.conf
explain
output
# demo service settings
host = 0.0.0.0
port = 8080

# logging
log_level := info
log_file := /var/log/demo/app.log
# max_connections = 100
timeout = 30

Put an address in front of s to restrict it. Only the two lines starting with log_ get the new separator; host, port and timeout are untouched.

Reorder text with capture groups

sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' data/sales.csv | head -4
explain
output
date,region,product,units,price
01/03/2026,north,widget,12,4.50
01/03/2026,south,gadget,3,19.99
02/03/2026,east,widget,7,4.50

With -E, parentheses capture and \1, \2, \3 refer back to them in the replacement. The year, month and day are captured and rewritten as day/month/year. The slashes in the replacement are escaped because / is the delimiter. The header line has no date and is unchanged. Output is cut to four lines with head.

Reuse the whole match with &

sed 's/[0-9][0-9]*ms/[&]/' logs/app.log | grep ms
explain
output
2026-03-14 08:16:40 ERROR health check failed: database timeout after [5000ms]

In the replacement, & stands for everything the pattern matched, so 5000ms becomes [5000ms]. Write \& for a literal ampersand.

Run several edits in one pass

sed -e 's/fix/repair/' -e '/milk/d' -e '$a\ship it' todo.txt
explain
output
repair login bug
write tests
repair logout bug
deploy v2.4
ship it

Each -e adds a command to the same script, applied in order to every line. fix is replaced, the milk line is deleted, and $a\ appends a new line after the last line. A ; between commands ('s/a/b/; /x/d') works too.

Insert a line before a match

sed '/^timeout/i max_connections = 200' data/settings.conf
explain
output
# demo service settings
host = 0.0.0.0
port = 8080

# logging
log_level = info
log_file = /var/log/demo/app.log
# max_connections = 100
max_connections = 200
timeout = 30

i inserts text before each line the address selects, and a appends after. The one-line form i text is a GNU extension; POSIX sed needs i\ and the text on the next line.

Strip Windows line endings

sed 's/\r$//' data/windows.txt | od -c | head -3
explain
output
0000000   l   i   n   e       o   n   e  \n   l   i   n   e       t   w
0000020   o  \n
0000022

Each line of windows.txt ends in carriage return plus newline. \r$ matches the carriage return at the end of the line, and od -c confirms only \n remains. \r in a regex is a GNU sed feature.

Quit after printing a line

sed -n '2{p;q}' data/users.csv
explain
output
1,Alice Moreau,alice@example.com,engineering,2021-04-12

Braces group commands under one address: on line 2, print it and quit. q stops reading at once, which matters on huge files where sed -n 2p would read to the end.

Common pitfalls

  • sed -i with no backup suffix overwrites the file, and a wrong regex can wipe its contents. Try the command without -i first, or use -i.bak.

  • Double quotes let the shell expand $ and backslashes before sed sees the script. Use single quotes, and switch to double only to insert a shell variable: sed "s/old/$new/" (and that breaks if $new contains / or &).

  • sed -n '/x/p' and sed '/x/p' differ: without -n every line prints once and matching lines print twice.

  • In basic regex +, ?, ( and | are literal. sed 's/a+/b/' only matches the text a+. Use -E or escape them (\+, a GNU extension).

  • sed -i replaces the file with a new one: a symlink is replaced by a regular file unless you add --follow-symlinks, and hard links to the old file keep the old content.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • In-place editing differs: GNU sed -i takes an optional attached suffix, BSD/macOS sed -i requires a suffix argument, so use sed -i '' 's/a/b/' file there for no backup. sed -i.bak works on both.

  • Escapes such as \t and \r in a pattern, and the one-line a text / i text forms, are GNU extensions. The macOS sed wants a literal tab and a\ with the text on the next line.

  • -E selects extended regex in both GNU and BSD sed; prefer it over the GNU spelling -r in scripts meant to be portable.

Options worth knowing

A selection, described in our own words. For the full list, see sed(1) or sed --help.

OptionWhat it does
-n, --quietDo not print each line automatically; only print what p asks for.
-e, --expression SCRIPTAdd SCRIPT to the commands to run; repeatable.
-f, --file FILERead the sed script from FILE.
-i, --in-place[=SUFFIX]Edit files in place, keeping a backup with SUFFIX if given.
-E, --regexp-extendedUse extended regular expressions.
-rSame as -E (GNU spelling).
-s, --separateTreat each file separately, so $ and line numbers restart per file.
-z, --null-dataSeparate records with NUL bytes instead of newlines.
--follow-symlinksWith -i, edit the file a symlink points to instead of replacing the link.
-u, --unbufferedFlush output after every line, for use in live pipelines.
--posixDisable GNU extensions.

Related