Skip to content

sed: Doing More

One substitution is a party trick. This chapter makes sed a tool: several commands in one pass, scripts saved to files, replacements that reuse what the pattern found, and — with its guardrails stated first — editing the file itself.

Several commands at once

Repeat -e to queue more than one edit. sed applies them in order, line by line, each command seeing the result of the one before:

$ sed -n '1p;5p' app.log | sed -e 's/INFO/I/' -e 's/WARN/W/'
2026-10-17 06:00:01 I cron job nightly-backup started
2026-10-17 08:41:03 W auth login failed user=admin ip=198.51.100.23

When the edits grow past two or three, they belong in a script file — one command per line, run with -f. A file that normalises the log's auth vocabulary:

$ cat fix.sed
s/login failed/LOGIN FAILED/
s/login ok/LOGIN OK/
s/logout/LOGOUT/
$ sed -f fix.sed app.log | sed -n '3p;15p'
2026-10-17 07:15:22 INFO auth LOGIN OK user=paul ip=192.168.1.10
2026-10-17 16:05:26 INFO auth LOGOUT user=paul ip=192.168.1.10
$ sed -f fix.sed app.log | grep -c LOGIN
18

18 lines carry a normalised marker (11 failed logins + 7 good ones). A sed script is a document: it can be kept, reviewed, and re-run next month — which the command line history is not. (fix.sed is yours to make, like chapter 3's patterns.txt.) Two commands may also be separated by a semicolon inside one set of quotes — s/a/b/; s/c/d/ — which the recipes chapter uses; -e and -f are the forms that stay readable.

Remembering what matched: groups and &

A substitution can reuse pieces of what it matched. Wrap part of the pattern in \( … \) — a capture group — and the replacement can refer to it as \1 (the first group), \2, and so on. Pull the username out of the log's key–value tail and bracket it:

$ grep 'login ok' app.log | sed 's/user=\([a-z]*\)/[\1]/'
2026-10-17 07:15:22 INFO auth login ok [paul] ip=192.168.1.10
2026-10-17 10:22:08 INFO auth login ok [katherine] ip=192.168.1.25

(and five more). The group captured paul or katherine, and the replacement rebuilt that stretch of line around it — user= and the value are gone, the brackets are new, \1 is the rescued text. Two groups can even change places, which is how you reformat a date stamp without a program:

$ sed 's/^\([0-9-]*\) \([0-9:]*\)/\2 \1/' app.log | head -3
06:00:01 2026-10-17 INFO cron job nightly-backup started
06:04:37 2026-10-17 INFO cron job nightly-backup finished status=ok
07:15:22 2026-10-17 INFO auth login ok user=paul ip=192.168.1.10

Group 1 is the date, group 2 is the time, and \2 \1 prints them swapped. The backslashes are the basic dialect again — sed's default, like grep's. GNU sed's -E switches patterns to the extended dialect, where groups are plain parentheses:

$ grep heartbeat app.log | sed -E 's/ip=([0-9.]+)/address=\1/'
2026-10-17 22:03:19 INFO app heartbeat ok address=10.0.0.5
2026-10-18 22:03:19 INFO app heartbeat ok address=10.0.0.5
2026-10-19 22:03:19 INFO app heartbeat ok address=10.0.0.5

One more replacement tool needs no groups at all: & in a replacement stands for the whole match. Marking every error line is one short command:

$ grep ERROR app.log | sed 's/ERROR/[&]/'
2026-10-17 09:58:47 [ERROR] app job import-orders failed reason=timeout
2026-10-17 15:44:10 [ERROR] http POST /orders 500 ip=203.0.113.7

(and eight more). & is also a trap when you don't mean it — chapter 10 shows a replacement going wrong by exactly one ampersand, and its escape, \&.

Editing the file: -i and the backup habit

Everything so far printed a stream. -i (in-place) tells sed to write the result back over the input file. It is the most useful option in this guide and the only one that can destroy data, so it gets a worked example with the safety habit built in: give -i a suffix. GNU sed takes the suffix attached, -i.bak, and keeps the pre-edit file under that name:

$ cp messy.txt scratch.txt
$ sed -i.bak 's/ *$//' scratch.txt
$ ls scratch.txt*
scratch.txt
scratch.txt.bak

The command trims trailing spaces — *$ is "any run of spaces at the end of the line," a tidy-up worth the whole chapter — and the proof that it worked, and only that, is one diff away:

$ diff scratch.txt.bak scratch.txt
2c2
< Apples 
---
> Apples
6c6
< carrots 
---
> carrots
10c10
< elderberries 
---
> elderberries
13c13
< grapes  
---
> grapes

Four lines changed, each by exactly the trailing spaces (diff exits 1 when files differ — that is its way of saying "found some," not an error). cat -A, which shows line-ends as $, confirms the spaces are gone:

$ cat -A scratch.txt | head -4
apples$
Apples$
 APPLES$
bananas$

The habit to keep: while an edit is new, always use the suffix, check with diff, and delete the .bak when you're satisfied. Two honest warnings. First, sed -i works by writing a fresh file and renaming it over the original — permissions and ownership can change, and a second hard link to the old file keeps the old text. Second, the spelling is a dialect trap: BSD sed (macOS) requires the suffix argument, so the same command there is sed -i '' 's/…/…/' file for no backup, and GNU's attached -i.bak reads differently to it. That difference is stated here from the tools' documentation, not from a capture — criteria.md records it as documented-only, as it does for the other BSD spellings in this guide.

Named, not taught: the hold space

sed carries a second storage area, the hold space, with commands (h, H, g, G, x) for parking a line and bringing it back later — enough machinery to join, reorder, and accumulate lines across a file. It is real, it is used, and it is deliberately out of this guide's scope: hold-space programs are the least readable text processing there is, and nearly every mortal job that reaches for it is clearer in awk, which holds whole tables in arrays (next chapter but one). Knowing the name exists — and that a sed incantation you meet in the wild with h and x in it is doing cross-line state — is the mortal share of it.

Next: awk, where lines become fields and fields become arithmetic.