When a config has the wrong hostname, a log line needs one field stripped, or a pipeline dumps text you want reshaped before it hits a file, opening an editor is the slow path. That is what sed is for. It reads a stream (or a file), applies edit instructions line by line, and writes the result to standard output.

You do not need every sed program on day one. Start with a plain substitution, then layer on global replace, addresses, in-place edits, and capture groups as the job gets fussier. The sections below walk through the commands you will reach for most often — with enough context that each one feels intentional, not magical.

Warm-up: substitute one string

Before diving into flags, give yourself a small playground. The short inventory below mirrors what you see in real configs and export files: hostnames, environments, and a couple of paths mixed with plain labels.

Create it once, then reuse it for every example in this article:

cat > hosts.txt <<'EOF'
web01.example.com prod /var/www/app
web02.example.com prod /var/www/app
db01.example.com staging /var/lib/postgres
cache01.example.com prod /var/cache/redis
legacy.example.com deprecated /opt/old/app
EOF

With the file ready, replace example.com with internal.net on each line. This is the simplest useful form of sed: the s (substitute) command, with a search pattern and a replacement.

sed 's/example.com/internal.net/' hosts.txt

You should see every hostname rewritten, while the rest of each line stays intact:

web01.internal.net prod /var/www/app
web02.internal.net prod /var/www/app
db01.internal.net staging /var/lib/postgres
cache01.internal.net prod /var/cache/redis
legacy.internal.net deprecated /opt/old/app

Note: By default sed prints every line to stdout and leaves the original file untouched. Treat the first pass as a dry run — inspect the output, then decide whether to redirect or edit in place.

Replace every match on a line

Without the g (global) flag, sed only rewrites the first match on each line. That is fine for hostnames that appear once, but it surprises you when the same token shows up twice.

Make the difference obvious with a one-liner that has two copies of prod:

echo 'prod web prod cache' | sed 's/prod/live/'

Only the first prod changes:

live web prod cache

Add g so every match on the line is replaced:

echo 'prod web prod cache' | sed 's/prod/live/g'
live web live cache

On the sample file, global replace is the same idea — useful when you expect repeats or when you are scrubbing a token that can appear mid-path:

sed 's/example.com/internal.net/g' hosts.txt

Note: Reach for g whenever “change all of these on the line” is the intent. Leave it off when you deliberately want only the first hit — for example, rewriting a leading field and leaving later ones alone.

Delimiters that are not slash

The classic s/old/new/ form gets noisy when the text itself contains /. Paths are the usual culprit: every slash in the pattern wants an escape, and the command becomes hard to read.

Change /var/www/app to /srv/www/app using | as the delimiter instead:

sed 's|/var/www/app|/srv/www/app|' hosts.txt

The two web rows update; other paths stay as they were:

web01.example.com prod /srv/www/app
web02.example.com prod /srv/www/app
db01.example.com staging /var/lib/postgres
cache01.example.com prod /var/cache/redis
legacy.example.com deprecated /opt/old/app

Any character that does not appear in the pattern can be the delimiter — #, @, and | are common picks.

Note: Prefer a different delimiter over a forest of \/ escapes. Clarity beats cleverness when you revisit the one-liner six months later.

sed is not only a find-and-replace tool. Addresses select which lines an instruction applies to, and d deletes matches from the output stream.

By default sed prints every line. Pair -n (quiet) with p (print) to show only the lines you ask for — here, lines that contain staging:

sed -n '/staging/p' hosts.txt
db01.example.com staging /var/lib/postgres

Line numbers and ranges work the same way. Print lines 2 through 4:

sed -n '2,4p' hosts.txt
web02.example.com prod /var/www/app
db01.example.com staging /var/lib/postgres
cache01.example.com prod /var/cache/redis

To drop lines instead of keeping them, use d. Remove every deprecated row:

sed '/deprecated/d' hosts.txt
web01.example.com prod /var/www/app
web02.example.com prod /var/www/app
db01.example.com staging /var/lib/postgres
cache01.example.com prod /var/cache/redis

You can also combine an address with a substitution so only matching lines change:

sed '/staging/s/example.com/internal.net/' hosts.txt

Only the staging host is rewritten; prod and deprecated rows keep example.com.

Note: Prefer sed '/pattern/d' when the job is “drop these lines from the stream.” Prefer grep -v when you are purely filtering and never planning to substitute on the keepers — use whichever reads clearer in the surrounding pipeline.

Edit a file in place

Once the dry-run output looks right, you can write the change back to the file with -i. On GNU sed, -i.bak keeps a backup beside the original so you can recover if the pattern was wrong.

Rewrite the domain and keep hosts.txt.bak:

sed -i.bak 's/example.com/internal.net/g' hosts.txt

Confirm the live file and the backup:

head -n 2 hosts.txt
head -n 2 hosts.txt.bak

Restore the playground from the backup when you want a clean slate for later examples:

mv hosts.txt.bak hosts.txt

On macOS BSD sed the backup suffix is required as a separate argument (sed -i '' 's/a/b/' file for no backup). If you share scripts across machines, test -i on the target platform or stick to redirect-and-replace:

sed 's/example.com/internal.net/g' hosts.txt > hosts.txt.tmp && mv hosts.txt.tmp hosts.txt

Note: Never point bare -i at a file you cannot recreate until you have verified the substitution on stdout. A wrong s///g plus in-place edit is how configs lose the only good copy.

Chain expressions and capture groups

Real edits often need more than one pass. -e lets you stack instructions in a single sed invocation so you do not pipe sed into sed.

Deprecate the legacy host and rewrite staging in one go:

sed -e '/legacy/d' -e '/staging/s/staging/qa/' hosts.txt
web01.example.com prod /var/www/app
web02.example.com prod /var/www/app
db01.example.com qa /var/lib/postgres
cache01.example.com prod /var/cache/redis

Capture groups pull pieces of the match into the replacement. Basic regular expressions (the default) use \(...\) and \1, \2, and so on. Extract just the hostname (the first whitespace-separated field):

sed 's/\([^ ]*\).*/\1/' hosts.txt
web01.example.com
web02.example.com
db01.example.com
cache01.example.com
legacy.example.com

With GNU sed, -E (or -r) enables extended regex so you can write (...) without the backslashes:

sed -E 's/([^ ]+).*/\1/' hosts.txt

Same hostnames, less escaping.

Note: Stick to one dialect in a script. Mixing BRE \( and ERE ( in the same head is a common source of “it worked in the shell but failed in CI” bugs.

Reshape pipeline output

sed shines in the middle of a pipe: take whatever grep, cut, or a command dumped, then rewrite it before the next stage. Suppose you only care about prod hosts and want a host=… assignment form for a script.

Filter to prod, then reshape the first field:

grep ' prod ' hosts.txt | sed -E 's/^([^ ]+).*/host=\1/'
host=web01.example.com
host=web02.example.com
host=cache01.example.com

Strip a noisy prefix from log-style lines the same way — keep everything after a marker token:

echo '2026-08-05 ERROR failed to connect: timeout' | sed 's/.*ERROR //'
failed to connect: timeout

That pattern pairs naturally with grep when you want unique error messages:

grep ERROR app.log | sed 's/.*ERROR //' | sort -u

Note: If you only need a fixed column and the delimiter is simple, cut or awk may be clearer. Reach for sed when the transformation is a regex rewrite, not a field split.

Quick reference card

Keep this nearby until the flags become muscle memory:

GoalCommand
Substitute first matchsed 's/old/new/' file
Substitute all on linesed 's/old/new/g' file
Alternate delimitersed 's#/old/path#/new/path#' file
Print matching linessed -n '/pat/p' file
Print line rangesed -n '2,4p' file
Delete matching linessed '/pat/d' file
Substitute on addresssed '/pat/s/a/b/' file
In-place with backupsed -i.bak 's/a/b/' file
Multiple expressionssed -e 'cmd1' -e 'cmd2' file
Extended regexsed -E 's/(a)/(b)/' file
Capture / backrefsed 's/\(x\)/\1/' file
From a pipecmd | sed 's/a/b/'

Practice drills

Use the sample hosts.txt (recreate it from the warm-up if you overwrote it) and try these without peeking. The point is to choose the edit with intent, not to memorize syntax under pressure.

  1. Replace every prod with production, but only on lines that already contain prod.
  2. Delete the legacy/deprecated row and print what remains.
  3. Print only the path column (the third field) using a capture-based substitution.
  4. Change /var/lib/postgres to /data/postgres without escaping slashes in the usual s/// way.
  5. Write a dry-run one-liner that rewrites example.com to internal.net globally, then an in-place version that keeps a .bak backup.

When you are ready to compare, here are solid answers — not the only ones, but clear and portable:

sed '/prod/s/prod/production/g' hosts.txt
sed '/deprecated/d' hosts.txt
sed -E 's/[^ ]+ [^ ]+ (.*)/\1/' hosts.txt
sed 's|/var/lib/postgres|/data/postgres|' hosts.txt
sed 's/example.com/internal.net/g' hosts.txt
sed -i.bak 's/example.com/internal.net/g' hosts.txt

If you can work through those five comfortably, you already cover most real sed work: dry-run substitutes, address-aware deletes, path-safe delimiters, capture-based reshaping, and safe in-place edits. Start with a plain s///, then tighten with g, addresses, and -i only when the first pass gets you most of the way there.