Text Processing · about 45 min

Editing streams with sed

Why this matters

You need to change port=8080 to port=9090 in a config file on twenty servers, rename a function across a project, turn American dates into European ones in an export, or strip the comments from a file to see what is actually set. Opening an editor does not scale and cannot be scripted. sed, the stream editor, applies editing commands to text automatically, line by line, and is one of the most used tools in shell scripts.

The model: a loop over lines

sed reads its input one line at a time. For each line it:

  1. reads the line (without its newline) into a buffer called the pattern space,
  2. runs your script's commands on it, in order,
  3. prints the pattern space (unless you used -n),
  4. moves on to the next line.
  input ─▶ [ read line into pattern space ] ─▶ [ run commands ] ─▶ [ print ] ─▶ output
               ▲                                                       │
               └───────────────────── next line ◀──────────────────────┘

sed never loads the whole file, so it handles any size, and like the other filters it reads files named as arguments or standard input, and writes standard output. The input file is not changed (unless you ask for -i, below).

The substitute command

Almost all sed use is the s command: s/REGEX/REPLACEMENT/FLAGS.

$ echo 'hello world' | sed 's/world/there/'
hello there
$ echo 'banana' | sed 's/a/o/'
bonana
$ echo 'banana' | sed 's/a/o/g'
bonono

Without flags, only the first match on each line is replaced. The flags:

The pattern is a regular expression, the same BRE dialect as grep by default; sed -E switches to extended regex, exactly like grep -E. Use single quotes around the whole script so bash leaves it alone.

In the replacement, & means "the whole matched text":

$ echo 'order 1042' | sed -E 's/[0-9]+/#&/'
order #1042

Other delimiters

The / is just the conventional delimiter. Any character after s works, which saves escaping when the text contains slashes:

$ echo '/usr/local/bin' | sed 's#/usr/local#/opt#'
/opt/bin

Capture groups and backreferences

Parentheses in the pattern (with -E) capture what they match; \1, \2, ... in the replacement insert the captured text:

$ echo 'Lovelace, Ada' | sed -E 's/([A-Za-z]+), ([A-Za-z]+)/\2 \1/'
Ada Lovelace
$ echo 'due 2026-11-05' | sed -E 's#([0-9]{4})-([0-9]{2})-([0-9]{2})#\3/\2/\1#'
due 05/11/2026

Read the second one slowly: group 1 is the year, 2 the month, 3 the day; the replacement puts them back as day/month/year. Groups are numbered by their opening parenthesis, left to right.

Addresses: choosing lines

Any sed command can be prefixed by an address that limits which lines it applies to:

AddressSelects
3line 3
$the last line
2,5lines 2 through 5
/regex/lines matching regex
/start/,/end/from a line matching start to the next line matching end
addr!every line not selected by addr

Open the shell for this lesson and try these on app.conf:

$ sed -n '4,6p' app.conf
name=demo
port=8080
debug=false
$ sed '/^#/d' app.conf
name=demo
port=8080
debug=false

log_level=info
log_file=/var/log/demo.log
$ sed '/^log_/s/^/# disabled: /' app.conf | tail -n 2
# disabled: log_level=info
# disabled: log_file=/var/log/demo.log

The commands used here:

Other single-letter commands exist (q to quit early, i and a to insert and append text, y to translate characters like tr), and sed has a full programming model with a second buffer (the hold space). Beyond s, d, p and addresses, most people reach for awk instead.

Several commands can be given with multiple -e options, or separated by ;:

$ sed -e '/^#/d' -e '/^$/d' defaults.conf
$ sed '/^#/d; /^$/d' defaults.conf

Editing files in place: -i

sed -i writes the result back to the file:

$ sed -i 's/^debug=false$/debug=true/' app.conf
$ grep debug app.conf
debug=true

Underneath, -i does not edit the file where it lies. sed writes the output to a new temporary file in the same directory and then renames it over the original. Two consequences: the file gets a new inode (hard links to the old one keep the old contents), and you need write permission on the directory, not just the file. -i.bak keeps a backup copy of the original named app.conf.bak, a good idea until you trust your expression.

Warning: sed -i has no undo. Run the command without -i first, look at the output, then add -i. (On macOS/BSD sed, -i needs an argument: sed -i '' .... Scripts meant to run on both are a portability headache.)

Anchoring: change only what you mean

A substitution matches anywhere in the line. s/port=8080/port=9090/ also rewrites a comment saying # the default was port=8080. Use anchors: ^port=8080$ matches only a line that is exactly that setting. This is the most important habit for safe sed scripts.

Common mistakes

Exercises

A quiz on the g flag, two config-file tasks in the shell (one in place, one to a new file), and two filter scripts.

Recap

Going further: try sed -n '/ERROR/,+2p' on a log (GNU extension: a match and the two lines after it), and sed '1~2d' (delete every other line). The GNU sed manual is available as info sed on systems that have it.

Exercises

Exercise 1 · quiz

How many replacements?

What does this print? Type the output.

echo 'banana' | sed 's/a/o/'

Answer

bonana. Without a flag, s replaces only the first match on each line. Add g (global) to replace all of them: sed 's/a/o/g' prints bonono. A number as flag replaces only that occurrence: s/a/o/2 gives banona.

Exercise 2 · task in a shell

Change a config file in place

Open the shell for this lesson. /work/app.conf is a configuration file. Using sed -i, change it in place so that

Everything else in the file, comments included, must stay exactly as it is. Note that a comment line also mentions port=8080; it must not change. Anchor your patterns.

Hint 1

sed -i 's/old/new/' file edits the file itself. ^ anchors a pattern to the start of the line.

Hint 2

sed -i -e 's/^debug=false$/debug=true/' -e 's/^port=8080$/port=9090/' app.conf

Reference solution
sed -i -e 's/^debug=false$/debug=true/' -e 's/^port=8080$/port=9090/' app.conf

Exercise 3 · task in a shell

Strip comments and blank lines

/work/defaults.conf is a long, heavily commented config file. Write its effective settings into /work/clean.conf: the same lines in the same order, but without lines that start with # and without empty lines. Leave defaults.conf itself unchanged.

Hint 1

The d command deletes the lines selected by its address: sed '/^#/d' removes comment lines.

Hint 2

Two expressions: sed -e '/^#/d' -e '/^$/d' defaults.conf > clean.conf

Reference solution
sed -e '/^#/d' -e '/^$/d' defaults.conf > clean.conf

Exercise 4 · write code

Reformat dates

Write a script that reads text from standard input and rewrites every date in the form YYYY-MM-DD to the form DD/MM/YYYY, leaving everything else unchanged. A line may contain several dates; convert them all.

Example: due 2026-11-05, paid 2026-11-01 becomes due 05/11/2026, paid 01/11/2026. Use sed -E with capture groups.

File: script.sh

Starting point

sed -E 's/-/\//g'

Example

input

due 2026-11-05, paid 2026-11-01

expected output

due 05/11/2026, paid 01/11/2026

Example

input

invoice 42
sent 2025-12-31
phone 555-1234

expected output

invoice 42
sent 31/12/2025
phone 555-1234
Hint 1

Parentheses in the pattern capture text; \1, \2, \3 in the replacement insert it again.

Hint 2

Use another delimiter so the slashes need no escaping: sed -E 's#([0-9]{4})-([0-9]{2})-([0-9]{2})#\3/\2/\1#g'

Reference solution
sed -E 's#([0-9]{4})-([0-9]{2})-([0-9]{2})#\3/\2/\1#g'

Exercise 5 · write code

Print a range of lines

Write a script that prints only lines 2 to 4 (inclusive) of its standard input, using a single sed command. If the input has fewer lines, print whatever exists in that range.

File: script.sh

Starting point

sed '2,4p'

Example

input

one
two
three
four
five
six

expected output

two
three
four

Example

input

a
b
c

expected output

b
c
Hint 1

The starter prints every line once (sed's automatic output) plus lines 2-4 a second time.

Hint 2

-n turns off the automatic printing, so only p prints: sed -n '2,4p'

Reference solution
sed -n '2,4p'