Linux sed Command
last modified October 23, 2026
The sed (stream editor) command edits text non-interactively.
It reads its input line by line, applies a script of editing commands to
each line, and writes the result to standard output. It is used to
substitute text, delete or select lines, insert lines, and edit
configuration files from scripts, often as part of a pipeline.
The syntax is sed [options] 'script' [file...]. Without
-i, the input files are never changed. Internally, sed reads
each line into the pattern space, runs the script on it, prints the
pattern space, and continues with the next line; a second buffer, the
hold space, keeps text between lines. The examples use GNU sed 4.9;
differences to BSD sed on macOS are noted at the end.
Table of contents
- Substitution: sample files, the s command, flags, delimiters, groups and &
- Selecting lines: printing with -n, addresses, deleting lines
- Scripts and files: multiple commands, script files, in-place editing, inserting lines, reading and writing files
- More commands: transliteration, case conversion, quitting, line numbers
- Advanced: joining lines, hold space, loops, whole-file editing
- Usage: shell variables, recipes, GNU and BSD sed
Sample files
Most examples work with a configuration file and an application log. A
CSV file of users with the columns id,name,email,city is also
used.
cat config.ini cat app.log
The configuration file has comments, an empty line, and two sections. Each log line has a date, a time, a level, a component in brackets, and a message.
$ cat config.ini # Application settings [server] host = localhost port = 8080 # debug = true [database] name = shop user = admin password = secret $ cat app.log 2026-10-23 08:00:01 INFO [web] server started on port 8080 2026-10-23 08:00:05 INFO [db] connected to shop 2026-10-23 08:01:12 WARN [web] slow response: 1840 ms 2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms 2026-10-23 08:02:31 INFO [db] retrying query 2026-10-23 08:03:45 ERROR [web] upstream returned 502 2026-10-23 08:04:00 INFO [web] GET /index.html 200
The s command
The substitute command s/pattern/replacement/ replaces the
first match of a regular expression on each line.
echo "hello world" | sed 's/world/there/' sed 's/localhost/db.example.com/' config.ini | grep host
sed reads standard input or the files given as arguments. The script is put in single quotes so that the shell does not interpret its characters. The file itself is unchanged; the result goes to standard output.
$ echo "hello world" | sed 's/world/there/' hello there $ sed 's/localhost/db.example.com/' config.ini | grep host host = db.example.com
Substitution flags
Flags after the last delimiter control which matches are replaced.
echo "one two two two" | sed 's/two/2/' echo "one two two two" | sed 's/two/2/g' echo "one two two two" | sed 's/two/2/2' echo "one two two two" | sed 's/two/2/2g' echo "Error error ERROR" | sed 's/error/fail/Ig'
Without a flag, only the first match on a line is replaced. g
replaces all matches, a number replaces only that occurrence, and a number
with g replaces that occurrence and all following ones. The
GNU flag I makes the match case-insensitive. Other flags are
p (print, see below) and w file (write the
changed line to a file).
$ echo "one two two two" | sed 's/two/2/' one 2 two two $ echo "one two two two" | sed 's/two/2/g' one 2 2 2 $ echo "one two two two" | sed 's/two/2/2' one two 2 two $ echo "one two two two" | sed 's/two/2/2g' one two 2 2 $ echo "Error error ERROR" | sed 's/error/fail/Ig' fail fail fail
Delimiters
The character after s is the delimiter. Any character can be
used, which helps with patterns that contain slashes.
echo "/usr/local/bin" | sed 's/\/usr\/local/\/opt/' echo "/usr/local/bin" | sed 's|/usr/local|/opt|' echo "/usr/local/bin" | sed 's#/#_#g'
All three commands work, but escaping every slash, as in the first one,
makes the expression hard to read. With | or #
as the delimiter, slashes need no escaping.
$ echo "/usr/local/bin" | sed 's/\/usr\/local/\/opt/' /opt/bin $ echo "/usr/local/bin" | sed 's|/usr/local|/opt|' /opt/bin $ echo "/usr/local/bin" | sed 's#/#_#g' _usr_local_bin
Groups and the matched text
In the replacement, & stands for the whole match, and
\1 to \9 for the parts captured by groups.
echo "port 8080" | sed 's/[0-9]*$/[&]/' echo "Anna Kovacova" | sed 's/\(.*\) \(.*\)/\2, \1/' echo "Anna Kovacova" | sed -E 's/(.*) (.*)/\2, \1/' sed -E 's/^([0-9-]+) ([0-9:]+) +([A-Z]+).*/\3 at \2/' app.log | head -n 3
By default, sed uses basic regular expressions (BRE), in which groups are
written as \( and \) and + and
? need a backslash. The -E option switches to
extended regular expressions (ERE), where they are written without
backslashes. The last command rearranges the log into level and time.
$ echo "port 8080" | sed 's/[0-9]*$/[&]/' port [8080] $ echo "Anna Kovacova" | sed 's/\(.*\) \(.*\)/\2, \1/' Kovacova, Anna $ echo "Anna Kovacova" | sed -E 's/(.*) (.*)/\2, \1/' Kovacova, Anna $ sed -E 's/^([0-9-]+) ([0-9:]+) +([A-Z]+).*/\3 at \2/' app.log | head -n 3 INFO at 08:00:01 INFO at 08:00:05 WARN at 08:01:12
Printing selected lines
The -n option turns off the automatic printing of each line.
Then only lines printed explicitly with p appear.
sed -n '/ERROR/p' app.log sed -n 's/.*\[\(db\)\] //p' app.log
/ERROR/p prints the lines that match, like
grep ERROR. Combined with the p flag of the
s command, -n prints only the lines where a
substitution happened; here, the messages of the db component
without their prefix.
$ sed -n '/ERROR/p' app.log 2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms 2026-10-23 08:03:45 ERROR [web] upstream returned 502 $ sed -n 's/.*\[\(db\)\] //p' app.log connected to shop query timeout after 5000 ms retrying query
Addresses
An address before a command selects the lines it applies to. It can be a line number, a regular expression, or a range of two addresses.
sed -n '2p' users.csv sed -n '2,3p' users.csv sed -n '$p' users.csv sed -n '/\[database\]/,$p' config.ini sed -n '1~2p' users.csv sed -n '0,/ERROR/p' app.log
$ is the last line. A range runs from the first address to the
second, inclusive; /\[database\]/,$ selects the database
section to the end of the file. The brackets are escaped because they are
special in regular expressions. The GNU extension first~step
selects every second line starting with the first. 0,/re/
ends the range at the first match, even when it is on line 1.
$ sed -n '2p' users.csv 1,Anna Kovacova,anna@example.com,Bratislava $ sed -n '2,3p' users.csv 1,Anna Kovacova,anna@example.com,Bratislava 2,Peter Novak,peter@example.com,Kosice $ sed -n '$p' users.csv 4,Martin Varga,martin@example.com,Nitra $ sed -n '/\[database\]/,$p' config.ini [database] name = shop user = admin password = secret $ sed -n '1~2p' users.csv id,name,email,city 2,Peter Novak,peter@example.com,Kosice 4,Martin Varga,martin@example.com,Nitra $ sed -n '0,/ERROR/p' app.log 2026-10-23 08:00:01 INFO [web] server started on port 8080 2026-10-23 08:00:05 INFO [db] connected to shop 2026-10-23 08:01:12 WARN [web] slow response: 1840 ms 2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms
Deleting lines
The d command deletes the selected lines. An !
after an address inverts it.
sed '1d' users.csv sed '/^#/d; /^$/d' config.ini sed '/\[database\]/!d' config.ini sed '/\[server\]/,/^$/!d' config.ini
The first command removes the CSV header, and the second removes comments
and empty lines, which leaves only the active settings. With
!, everything except the matching lines is deleted, so the
last command keeps only the server section up to the empty line.
$ sed '1d' users.csv 1,Anna Kovacova,anna@example.com,Bratislava 2,Peter Novak,peter@example.com,Kosice 3,Lucia Horvathova,lucia@example.com,Zilina 4,Martin Varga,martin@example.com,Nitra $ sed '/^#/d; /^$/d' config.ini [server] host = localhost port = 8080 [database] name = shop user = admin password = secret $ sed '/\[database\]/!d' config.ini [database] $ sed '/\[server\]/,/^$/!d' config.ini [server] host = localhost port = 8080
Multiple commands
Several commands are separated with semicolons or given with repeated
-e options. They run in order on each line.
sed -e 's/INFO /info /' -e 's/ERROR/error/' app.log | head -n 4 sed 's/,/;/g; 1d' users.csv
In the second command, the header line is first changed and then deleted;
the order matters only when one command affects what the next one sees.
The trailing space in 's/INFO /info /' keeps the columns of
the log aligned.
$ sed -e 's/INFO /info /' -e 's/ERROR/error/' app.log | head -n 4 2026-10-23 08:00:01 info [web] server started on port 8080 2026-10-23 08:00:05 info [db] connected to shop 2026-10-23 08:01:12 WARN [web] slow response: 1840 ms 2026-10-23 08:02:30 error [db] query timeout after 5000 ms $ sed 's/,/;/g; 1d' users.csv 1;Anna Kovacova;anna@example.com;Bratislava 2;Peter Novak;peter@example.com;Kosice 3;Lucia Horvathova;lucia@example.com;Zilina 4;Martin Varga;martin@example.com;Nitra
Script files
Longer scripts are easier to maintain in a file, one command per line, with comments.
# remove comments and blank lines /^#/d /^$/d # normalize "key = value" to "key=value" s/ *= */=/
The script removes comments and empty lines and removes the spaces around
the equals sign. The -f option reads the script from the
file.
sed -f clean.sed config.ini
A script file can also be made executable with the first line
#!/usr/bin/sed -f.
$ sed -f clean.sed config.ini [server] host=localhost port=8080 [database] name=shop user=admin password=secret
Editing files in place
The -i option writes the result back to the file. With a
suffix, as in -i.bak, the original is kept as a backup.
cp config.ini app.ini sed -i.bak 's/^port = .*/port = 9090/' app.ini diff app.ini.bak app.ini sed -i '/^password/d' app.ini grep -c password app.ini app.ini.bak
sed writes the output to a temporary file and renames it over the
original, so the file gets a new inode, and a symbolic link would be
replaced by a regular file unless --follow-symlinks is used.
Test a command without -i first, or keep a backup; a wrong
-i command cannot be undone.
$ cp config.ini app.ini $ sed -i.bak 's/^port = .*/port = 9090/' app.ini $ diff app.ini.bak app.ini 4c4 < port = 8080 --- > port = 9090 $ sed -i '/^password/d' app.ini $ grep -c password app.ini app.ini.bak app.ini:0 app.ini.bak:1
Inserting and changing lines
The i command inserts text before a line, a
appends text after it, and c replaces it.
sed '/^\[database\]/i # --- database section ---' config.ini | sed -n '5,9p' sed '/^port/a timeout = 30' config.ini | sed -n '2,6p' sed '/^password/c password = ********' config.ini | tail -n 2
The one-line form with the text right after the command is a GNU
extension. POSIX sed requires a backslash and a newline after the command,
for example a\ followed by the text on the next line. The
second sed -n in each pipeline only shows the relevant part of
the result.
$ sed '/^\[database\]/i # --- database section ---' config.ini | sed -n '5,9p' # debug = true # --- database section --- [database] name = shop $ sed '/^port/a timeout = 30' config.ini | sed -n '2,6p' [server] host = localhost port = 8080 timeout = 30 $ sed '/^password/c password = ********' config.ini | tail -n 2 user = admin password = ********
Reading and writing files
The r command inserts the contents of a file after a line, and
the w command writes the selected lines to a file.
printf '# generated file, do not edit\n' > header.txt sed '1r header.txt' users.csv | head -n 3 sed -n '/ERROR/w errors.log' app.log cat errors.log
1r header.txt adds the header after line 1. The
w command splits the log, saving the errors to a separate file
in the same pass; with -n, nothing is printed to standard
output.
$ printf '# generated file, do not edit\n' > header.txt $ sed '1r header.txt' users.csv | head -n 3 id,name,email,city # generated file, do not edit 1,Anna Kovacova,anna@example.com,Bratislava $ sed -n '/ERROR/w errors.log' app.log $ cat errors.log 2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms 2026-10-23 08:03:45 ERROR [web] upstream returned 502
Transliteration
The y command replaces characters one by one, like
tr.
echo "hello world" | sed 'y/abcdefghij/0123456789/' echo "Bratislava" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
Each character in the first list is replaced with the character at the same
position in the second list; both lists must be equally long. Ranges such
as a-z are not supported, so the letters must be listed.
$ echo "hello world" | sed 'y/abcdefghij/0123456789/' 74llo worl3 $ echo "Bratislava" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' BRATISLAVA
Case conversion
GNU sed supports case conversion in the replacement: \U and
\L convert the rest of the replacement to uppercase or
lowercase, and \u and \l convert the next
character.
echo "hello world" | sed 's/.*/\U&/' echo "hello world" | sed 's/\b./\u&/g' sed -E 's/^([^,]*),([^,]*)/\1,\U\2/' users.csv | head -n 3
\b matches a word boundary, so \b. is the first
letter of each word, and the second command capitalizes the words. The
last command converts only the second CSV column; \E would
end the conversion if more text followed.
$ echo "hello world" | sed 's/.*/\U&/' HELLO WORLD $ echo "hello world" | sed 's/\b./\u&/g' Hello World $ sed -E 's/^([^,]*),([^,]*)/\1,\U\2/' users.csv | head -n 3 id,NAME,email,city 1,ANNA KOVACOVA,anna@example.com,Bratislava 2,PETER NOVAK,peter@example.com,Kosice
Quitting early
The q command prints the current line and stops. On large
files, this saves reading the rest of the input.
sed '3q' app.log
sed '/ERROR/q' app.log
sed -n '/ERROR/{p;q}' app.log
sed '/ERROR/q5' app.log > /dev/null; echo $?
3q works like head -n 3. Braces group several
commands for one address, so /ERROR/{p;q} prints only the
first error. GNU sed accepts an exit code after q, which a
script can test.
$ sed '3q' app.log
2026-10-23 08:00:01 INFO [web] server started on port 8080
2026-10-23 08:00:05 INFO [db] connected to shop
2026-10-23 08:01:12 WARN [web] slow response: 1840 ms
$ sed '/ERROR/q' app.log
2026-10-23 08:00:01 INFO [web] server started on port 8080
2026-10-23 08:00:05 INFO [db] connected to shop
2026-10-23 08:01:12 WARN [web] slow response: 1840 ms
2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms
$ sed -n '/ERROR/{p;q}' app.log
2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms
$ sed '/ERROR/q5' app.log > /dev/null; echo $?
5
Line numbers
The = command prints the current line number on a line of its
own.
sed -n '$=' app.log sed '=' users.csv | sed 'N; s/\n/ /' | head -n 3 sed -n '/ERROR/=' app.log sed -s -n '1p' users.csv config.ini
sed -n '$=' counts lines like wc -l. The second
command numbers the lines; a second sed joins each number with its line.
The third prints the line numbers of the errors. Normally, sed treats all
files as one stream; with -s, each file is separate, so
1p prints the first line of every file.
$ sed -n '$=' app.log 7 $ sed '=' users.csv | sed 'N; s/\n/ /' | head -n 3 1 id,name,email,city 2 1,Anna Kovacova,anna@example.com,Bratislava 3 2,Peter Novak,peter@example.com,Kosice $ sed -n '/ERROR/=' app.log 4 6 $ sed -s -n '1p' users.csv config.ini id,name,email,city # Application settings
Joining lines
The N command appends the next line to the pattern space,
separated by a newline, so a command can work on two lines at once.
printf 'name: Anna\ncity: Bratislava\nname: Peter\ncity: Kosice\n' | sed 'N; s/\n/ | /' printf 'apt-get install \\\n curl \\\n git\nls\n' > cmds.txt cat cmds.txt sed -e ':a' -e '/\\$/N; s/\\\n *//; ta' cmds.txt
The first command joins pairs of lines. The second joins lines that end
with a backslash, as in shell scripts: while the pattern space ends with
\, the next line is appended, the backslash, newline, and
indentation are removed, and ta jumps back to the label
:a. Labels and jumps are explained below.
$ printf 'name: Anna\ncity: Bratislava\nname: Peter\ncity: Kosice\n' | sed 'N; s/\n/ | /'
name: Anna | city: Bratislava
name: Peter | city: Kosice
$ printf 'apt-get install \\\n curl \\\n git\nls\n' > cmds.txt
$ cat cmds.txt
apt-get install \
curl \
git
ls
$ sed -e ':a' -e '/\\$/N; s/\\\n *//; ta' cmds.txt
apt-get install curl git
ls
The hold space
The hold space keeps text between lines. h copies the pattern
space to the hold space, G appends the hold space to the
pattern space, and x exchanges the two.
printf '1\n2\n3\n4\n' | sed -n '1!G; h; $p'
sed -n '/ERROR/{x;p;x;p}; h' app.log
The first command reverses the lines, like tac: each line,
except the first, gets the previous lines appended, the result is saved in
the hold space, and the last line prints everything. The second command
prints each error together with the line before it. Every line is saved
with h; on an error line, x;p;x prints the saved
previous line and swaps back, and p prints the error.
grep -B 1 does the same; the example shows how the hold space
works.
$ printf '1\n2\n3\n4\n' | sed -n '1!G; h; $p'
4
3
2
1
$ sed -n '/ERROR/{x;p;x;p}; h' app.log
2026-10-23 08:01:12 WARN [web] slow response: 1840 ms
2026-10-23 08:02:30 ERROR [db] query timeout after 5000 ms
2026-10-23 08:02:31 INFO [db] retrying query
2026-10-23 08:03:45 ERROR [web] upstream returned 502
Labels and loops
:label defines a label, b label jumps to it, and
t label jumps only if a substitution has succeeded since the
last input line or jump. This makes loops possible.
echo "1234567890" | sed -E ':a; s/([0-9])([0-9]{3})($|,)/\1,\2\3/; ta'
echo "a b c" | sed ':a; s/ / /; ta'
The first command inserts thousands separators: each substitution puts a
comma before the last group of three digits that has no comma yet, and
ta repeats it until nothing changes. The second command
squeezes runs of spaces the same way; s/ */ /g or
tr -s ' ' are simpler for that task.
$ echo "1234567890" | sed -E ':a; s/([0-9])([0-9]{3})($|,)/\1,\2\3/; ta'
1,234,567,890
$ echo "a b c" | sed ':a; s/ / /; ta'
a b c
Editing the whole input at once
The -z option separates records with null bytes instead of
newlines. For normal text, the whole input becomes a single record.
printf 'one\ntwo\nthree\n' | sed -z 's/\n/, /g; s/, $/\n/'
Because the newlines are now part of the pattern space, they can be
replaced like any other character. Here, the lines are joined with commas,
and the final comma is changed back to a newline. -z also
processes null-separated file lists, as produced by
find -print0.
$ printf 'one\ntwo\nthree\n' | sed -z 's/\n/, /g; s/, $/\n/' one, two, three
Using shell variables
To use a shell variable in a sed script, use double quotes. The value then becomes part of the script, which causes problems when it contains special characters.
port=9000 sed "s/^port = .*/port = $port/" config.ini | grep port dir=/var/www/html sed "s/^host = .*/host = $dir/" config.ini | grep host sed "s|^host = .*|host = $dir|" config.ini | grep host team="R&D" echo "team: TEAM" | sed "s/TEAM/$team/" team_esc=$(printf '%s\n' "$team" | sed 's/[&/\]/\\&/g') echo "team: TEAM" | sed "s/TEAM/$team_esc/"
The slashes in $dir end the s command too early;
a different delimiter fixes that. An & in the value is
replaced with the matched text, so R&D becomes
RTEAMD. The safe solution escapes &,
/, and \ in the value before using it.
$ port=9000 $ sed "s/^port = .*/port = $port/" config.ini | grep port port = 9000 $ dir=/var/www/html $ sed "s/^host = .*/host = $dir/" config.ini | grep host sed: -e expression #1, char 22: unknown option to `s' $ sed "s|^host = .*|host = $dir|" config.ini | grep host host = /var/www/html $ team="R&D" $ echo "team: TEAM" | sed "s/TEAM/$team/" team: RTEAMD $ team_esc=$(printf '%s\n' "$team" | sed 's/[&/\]/\\&/g') $ echo "team: TEAM" | sed "s/TEAM/$team_esc/" team: R&D
Recipes
These one-liners solve common text-editing tasks.
printf ' text with spaces \n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cat -A
echo '<p>Hello <b>world</b></p>' | sed 's/<[^>]*>//g'
printf 'line one\r\nline two\r\n' | sed 's/\r$//' | od -c | head -n 2
sed 's/^# *debug/debug/' config.ini | grep debug
sed '/^user/s/^/# /' config.ini | grep user
sed -n '/\[server\]/,/^\[/{/^\[/!p}' config.ini
The commands trim leading and trailing whitespace (cat -A
marks the line end with $), strip simple HTML tags, convert
Windows line endings, uncomment a setting, comment out a setting, and
print the body of the server section without the section headers. For
real HTML, use a proper parser; regular expressions handle only simple
cases.
$ printf ' text with spaces \n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cat -A
text with spaces$
$ echo '<p>Hello <b>world</b></p>' | sed 's/<[^>]*>//g'
Hello world
$ printf 'line one\r\nline two\r\n' | sed 's/\r$//' | od -c | head -n 2
0000000 l i n e o n e \n l i n e t w
0000020 o \n
$ sed 's/^# *debug/debug/' config.ini | grep debug
debug = true
$ sed '/^user/s/^/# /' config.ini | grep user
# user = admin
$ sed -n '/\[server\]/,/^\[/{/^\[/!p}' config.ini
host = localhost
port = 8080
# debug = true
GNU and BSD sed
macOS and the BSD systems ship a different sed. Scripts that must run on
both should avoid GNU extensions or install GNU sed (as
gsed).
- In-place editing: BSD sed requires an argument for
-i; usesed -i '' 's/a/b/' filethere, or-i.bak, which works on both. - Extended regex:
-Eworks on both;-ris GNU only. - GNU-only features: the
Iflag,\U/\L,first~step,0,/re/, one-linea/i/c,qwith an exit code,-z, and-s. - Escapes:
\nin the replacement,\t, and\bare GNU extensions.
Best practices for sed
- Test before -i: Run the command without
-iand check the output, or use-i.bak. - Quote the script: Use single quotes, and double quotes only when a variable is needed.
- Pick a readable delimiter: Use
|or#for paths. - Use -E: Extended regular expressions need fewer backslashes.
- Know when to switch tools: For field-based processing, use awk; for complex logic, use a scripting language.
Source
In this article, we have used the sed command to substitute
text, select, delete, and insert lines, edit files in place, convert case,
work with multiple lines and the hold space, write loops, and handle shell
variables safely.
Author
List all Linux tutorials.