sed Cheatsheet
In-Place Editing
Use this sed reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
-i is not portable, and this is why
-i is not in POSIX and the two implementations disagree about its argument.
| Invocation | GNU sed | BSD / macOS sed |
|---|---|---|
sed -i 's/a/b/' f | edits f, no backup | reads s/a/b/ as the suffix, then tries to run f as the script |
sed -i '' 's/a/b/' f | takes '' as the script, then cannot read the file s/a/b/ | edits f, no backup |
sed -i.bak 's/a/b/' f | edits f, keeps f.bak | edits f, keeps f.bak |
sed -i'' -e 's/a/b/' f | edits f, no backup | edits f, leaves a backup named f-e |
sed --in-place ... | works | not recognized |
The error message when you get it wrong is misleading. GNU reports a missing file
and BSD reports an undefined label, neither of which mentions -i.
sed -i 's/a/b/' t1 # BSD: sed: 2: "t1": undefined label '1' gsed -i '' 's/a/b/' t7 # GNU: can't read s/a/b/: No such file or directory
Worse, GNU can fail silently. gsed -i '' file reports nothing and exits 0,
because '' is a valid empty script and file is the operand, so it rewrites the
file with itself.
What to write instead
Four options, in order of how much you should trust them in a script that other people run.
# 1. a suffix both accept, then remove the backup sed -i.bak 's/a/b/' file && rm -f file.bak # 2. a temp file, explicit and reviewable sed 's/a/b/' file > file.tmp && mv file.tmp file # 3. perl, identical everywhere and no suffix dance perl -i -pe 's/a/b/' file # 4. detect once, reuse if sed --version >/dev/null 2>&1; then SEDI=(-i); else SEDI=(-i ''); fi sed "${SEDI[@]}" 's/a/b/' file
Never do this. The shell truncates the redirect target before sed opens it, so the file is empty before a single byte is read:
sed 's/a/b/' file > file # file is now 0 bytes, content gone
sponge from moreutils is the safe version of that shape:
sed 's/a/b/' file | sponge fileBackup suffixes
The suffix is appended, not inserted, so it needs its own dot if you want one.
-i.bak gives file.bak on both platforms and is the one portable spelling.
sed -i.bak 's/a/b/' file # file.bak, both sed -i.orig 's/a/b/' file # file.orig, both gsed -i'*.orig' 's/a/b/' file # file.orig, GNU: * is the original name mkdir -p bak gsed -i'bak/*' 's/a/b/' file # backups collected in bak/, GNU only
- A GNU suffix containing * is a template, and * expands to the file name. The
target directory must already exist or the rename fails.
- BSD treats the suffix as a literal string with no * expansion.
- A suffix with no leading dot is legal but easy to mistype into a label error, as
gsed -i bak 's/a/b/' f shows, because the detached bak becomes the script.
What -i does to the file itself
-i is not an edit in place. Both seds write a new temporary file and rename it
over the original. That has consequences.
| Property | Preserved by -i |
|---|---|
| permissions | yes, both platforms |
| ownership | yes when you own the file, root-owned needs root |
| hard links | no, the link is broken and the twin keeps the old content |
| symlinks | no, the symlink is replaced by a regular file |
| inode number | no, it changes |
| extended attributes | not reliably |
printf 'a\n' > h1 && ln h1 h2 gsed -i 's/a/b/' h1 cat h2 # still "a", the files are no longer linked gsed -i --follow-symlinks 's/a/b/' link # GNU only, edits the target
--follow-symlinks is GNU only. On macOS, resolve the path yourself before editing:
target=$(readlink link || printf '%s' link) sed -i.bak 's/a/b/' "$target" && rm -f "$target.bak"
Editing many files
One -i call takes any number of files, and each is edited independently. With
-i, $ means the last line of each file on both platforms. Without -i, $
is the last line of the last file, unless you pass GNU -s.
sed -i.bak 's/a/b/' *.txt # every file in the dir sed -i.bak 's/a/b/' f1 f2 f3 # explicit list # recursive, portable, one sed process per batch find . -name '*.md' -exec sed -i.bak 's/old/new/g' {} + find . -name '*.md.bak' -delete # GNU, recursive, skip binaries and .git grep -rlZ --exclude-dir=.git 'old' . | xargs -0 gsed -i 's/old/new/g' # ripgrep, only the files that actually match rg -l 'old' | xargs sed -i.bak 's/old/new/g'
- Filter with grep -rl or rg -l first. Running -i over every file rewrites
timestamps on files that did not change, which pollutes a git status and a build
cache.
- -exec ... {} + batches files into as few sed invocations as possible. {} \;
runs one process per file and is far slower on large trees.
- xargs without -0 breaks on paths containing spaces. Pair grep -rlZ with
xargs -0.
Dry run first
sed has no --dry-run, so the diff is the dry run. Do this before every bulk edit.
# see exactly what would change, file by file for f in *.txt; do diff -u "$f" <(sed 's/old/new/g' "$f") done # one-liner, whole tree git ls-files -z | xargs -0 -I{} sh -c \ 'diff -q "{}" <(sed "s/old/new/g" "{}") >/dev/null || echo "{}"' # or just edit, then review with git sed -i.bak 's/old/new/g' *.txt && rm -f *.bak && git diff
Under version control,
git diffplusgit checkout --is a better safety net than any backup suffix. Commit or stash before a bulk-i, then the undo is one command instead of afind -name '*.bak'cleanup.