sed: Editing a Text Stream Line by Line

Per GNU sed’s sed(1) man page, sed is a “stream editor for filtering and transforming text” — it reads input one line at a time into a working buffer (the “pattern space”), runs a script of edit commands against it, and by default prints the result before moving to the next line. This post runs five sed commands against a synthetic access log: substitution, line-range and pattern addressing, and deletion.

The data

All five sed calls below run against the same access.log, 30 lines, one simulated HTTP request per line in a Combined-Log-Format-style layout: client IP, two placeholder dashes, [timestamp], "METHOD PATH HTTP/1.1", status code, response size in bytes — fields separated by single spaces.

203.0.113.5 - - [24/Aug/2026:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:02 +0000] "GET /index.html HTTP/1.1" 200 532
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.23 - - [24/Aug/2026:10:00:05 +0000] "GET /static/app.js HTTP/1.1" 200 8213
192.0.2.77 - - [24/Aug/2026:10:00:07 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:09 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.23 - - [24/Aug/2026:10:00:12 +0000] "GET /favicon.ico HTTP/1.1" 404 0
203.0.113.5 - - [24/Aug/2026:10:00:14 +0000] "GET /api/users HTTP/1.1" 200 964
198.51.100.9 - - [24/Aug/2026:10:00:16 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
203.0.113.5 - - [24/Aug/2026:10:00:20 +0000] "DELETE /api/orders/44 HTTP/1.1" 204 0
198.51.100.23 - - [24/Aug/2026:10:00:22 +0000] "GET /old-page HTTP/1.1" 301 178
192.0.2.77 - - [24/Aug/2026:10:00:24 +0000] "GET /api/orders HTTP/1.1" 200 1180
203.0.113.5 - - [24/Aug/2026:10:00:26 +0000] "GET /static/app.js HTTP/1.1" 200 8213
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:30 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:32 +0000] "GET /admin HTTP/1.1" 403 96
198.51.100.23 - - [24/Aug/2026:10:00:34 +0000] "GET /index.html HTTP/1.1" 200 532
192.0.2.77 - - [24/Aug/2026:10:00:36 +0000] "PUT /api/users/9 HTTP/1.1" 200 210
203.0.113.5 - - [24/Aug/2026:10:00:38 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.9 - - [24/Aug/2026:10:00:40 +0000] "GET /favicon.ico HTTP/1.1" 404 0
192.0.2.77 - - [24/Aug/2026:10:00:42 +0000] "GET /static/app.js HTTP/1.1" 200 8213
203.0.113.5 - - [24/Aug/2026:10:00:44 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:46 +0000] "POST /login HTTP/1.1" 200 356
192.0.2.77 - - [24/Aug/2026:10:00:48 +0000] "GET /api/users HTTP/1.1" 200 964
203.0.113.5 - - [24/Aug/2026:10:00:50 +0000] "GET /old-page HTTP/1.1" 301 178
198.51.100.9 - - [24/Aug/2026:10:00:52 +0000] "GET /api/orders HTTP/1.1" 500 89
192.0.2.77 - - [24/Aug/2026:10:00:54 +0000] "GET /admin HTTP/1.1" 403 96
203.0.113.5 - - [24/Aug/2026:10:00:56 +0000] "GET /index.html HTTP/1.1" 200 532

The code

#!/usr/bin/env bash
set -e

echo "== substitute: mask IPs, replace octets with x =="
sed -E 's/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/x.x.x.x/' access.log | head -5

echo
echo "== address range: print lines 1-3 =="
sed -n '1,3p' access.log

echo
echo "== pattern address: print only /login lines =="
sed -n '/\/login/p' access.log

echo
echo "== delete: drop 200-status lines, print the rest =="
sed '/ 200 /d' access.log | head -5

echo
echo "== different delimiter (#), since the pattern itself contains / =="
sed 's#GET#FETCH#' access.log | head -2

Per the man page:

  • s/pattern/replacement/ — the substitute command; rewrites text matching pattern in the pattern space. -E switches on extended regular expressions (so + and grouping don’t need backslash-escaping).
  • -n, --quiet, --silent — “suppress automatic printing of pattern space,” so only lines an explicit p command prints show up — used below with an address so only addressed lines print.
  • Addressing — a command can be restricted to specific lines: 1,3p runs p only on lines 1 through 3; /pattern/p runs p only on lines matching pattern; /pattern/d runs the delete command d only on matching lines, removing them from the output.
  • Delimiter choices’s three parts are separated by whatever character follows the s, not necessarily /; the last example uses s#GET#FETCH# so the pattern/replacement can be written without escaping.

Running it

Real output:

== substitute: mask IPs, replace octets with x ==
x.x.x.x - - [24/Aug/2026:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 532
x.x.x.x - - [24/Aug/2026:10:00:02 +0000] "GET /index.html HTTP/1.1" 200 532
x.x.x.x - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
x.x.x.x - - [24/Aug/2026:10:00:05 +0000] "GET /static/app.js HTTP/1.1" 200 8213
x.x.x.x - - [24/Aug/2026:10:00:07 +0000] "GET /api/users HTTP/1.1" 200 964

== address range: print lines 1-3 ==
203.0.113.5 - - [24/Aug/2026:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:02 +0000] "GET /index.html HTTP/1.1" 200 532
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128

== pattern address: print only /login lines ==
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
203.0.113.5 - - [24/Aug/2026:10:00:09 +0000] "POST /login HTTP/1.1" 200 356
198.51.100.9 - - [24/Aug/2026:10:00:28 +0000] "POST /login HTTP/1.1" 401 128
198.51.100.23 - - [24/Aug/2026:10:00:46 +0000] "POST /login HTTP/1.1" 200 356

== delete: drop 200-status lines, print the rest ==
203.0.113.5 - - [24/Aug/2026:10:00:04 +0000] "POST /login HTTP/1.1" 401 128
192.0.2.77 - - [24/Aug/2026:10:00:11 +0000] "GET /api/orders HTTP/1.1" 500 89
198.51.100.23 - - [24/Aug/2026:10:00:12 +0000] "GET /favicon.ico HTTP/1.1" 404 0
192.0.2.77 - - [24/Aug/2026:10:00:18 +0000] "GET /api/orders HTTP/1.1" 500 89
203.0.113.5 - - [24/Aug/2026:10:00:20 +0000] "DELETE /api/orders/44 HTTP/1.1" 204 0

== different delimiter (#), since the pattern itself contains / ==
203.0.113.5 - - [24/Aug/2026:10:00:01 +0000] "FETCH /index.html HTTP/1.1" 200 532
198.51.100.23 - - [24/Aug/2026:10:00:02 +0000] "FETCH /index.html HTTP/1.1" 200 532

The IP-masking substitution replaced every line’s leading dotted-quad with x.x.x.x, leaving the rest of the line untouched. -n '1,3p' printed exactly the first three lines and nothing else — without -n, every line would have auto-printed once already before p printed the addressed ones again. -n '/\/login/p' picked out the same four /login lines grep would find. / 200 /d deleted every 200-status line, leaving 401s, 500s, a 404 and a 204 in the output. The last substitution swapped # in for / as the delimiter, replacing GET with FETCH on the first two lines shown.

Takeaway

This run showed three distinct sed jobs on the same log — rewriting text in place with s///, selecting specific lines with -n plus an address, and removing lines with d — and that the delimiter after s is arbitrary, useful when the pattern or replacement itself contains /.