ZetCode

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

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.

clean.sed
# 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).

Best practices for sed

Source

GNU sed manual

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.