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...]
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
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'
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
# 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
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
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
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
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
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
# 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
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
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
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
# 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
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
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 -iwith no backup suffix overwrites the file, and a wrong regex can wipe its contents. Try the command without-ifirst, 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$newcontains/or&).sed -n '/x/p'andsed '/x/p'differ: without-nevery line prints once and matching lines print twice.In basic regex
+,?,(and|are literal.sed 's/a+/b/'only matches the texta+. Use-Eor escape them (\+, a GNU extension).sed -ireplaces 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 -itakes an optional attached suffix, BSD/macOSsed -irequires a suffix argument, so usesed -i '' 's/a/b/' filethere for no backup.sed -i.bakworks on both.Escapes such as
\tand\rin a pattern, and the one-linea text/i textforms, are GNU extensions. The macOS sed wants a literal tab anda\with the text on the next line.-Eselects extended regex in both GNU and BSD sed; prefer it over the GNU spelling-rin 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.
| Option | What it does |
|---|---|
-n, --quiet | Do not print each line automatically; only print what p asks for. |
-e, --expression SCRIPT | Add SCRIPT to the commands to run; repeatable. |
-f, --file FILE | Read the sed script from FILE. |
-i, --in-place[=SUFFIX] | Edit files in place, keeping a backup with SUFFIX if given. |
-E, --regexp-extended | Use extended regular expressions. |
-r | Same as -E (GNU spelling). |
-s, --separate | Treat each file separately, so $ and line numbers restart per file. |
-z, --null-data | Separate records with NUL bytes instead of newlines. |
--follow-symlinks | With -i, edit the file a symlink points to instead of replacing the link. |
-u, --unbuffered | Flush output after every line, for use in live pipelines. |
--posix | Disable GNU extensions. |
Related
awkScan text as records and fields, and run a small program on each line that matches a pattern.grepPrint the lines of files (or standard input) that match a pattern.trTranslate, squeeze or delete characters from standard input; for case changes, line endings and stripping characters.cutPrint selected fields, characters or bytes from each line, such as columns of a CSV or TSV file.diffCompare two files or directories line by line and print the changes that turn one into the other.- QuotingSingle quotes, double quotes, backslashes and $'...', what each protects, and the unquoted-variable bugs they prevent, each shown failing and then fixed.