Learn how to effectively use the sed replace command for safe, portable editing in Linux and macOS. Master in-place substitutions today!
The sed substitution command takes the form s/regexp/replacement/flags. For safe, portable in-place editing on both Linux and macOS, use sed -i.bak 's/old/new/g' file && rm file.bak. That single pattern covers GNU sed and BSD sed without modification.
Pro Tip: Always dry-run first. Drop the -i.bak and pipe to your terminal to confirm the output looks right before writing anything to disk.
Key Takeaways
Portable, safe sed usage comes down to three rules: use s/regexp/replacement/flags with -i.bak for cross-platform in-place edits, dry-run before writing, and anchor regexes for idempotency in CI pipelines.
| Point | Details |
|---|---|
| Canonical syntax | s/REGEXP/REPLACEMENT/FLAGS is the standard form; any delimiter works. |
| Portable in-place edit | Use -i.bak on both GNU and BSD; clean up the backup after validation. |
| Dry-run first | Run without -i and pipe to diff before any file mutation. |
| Anchor for idempotency | Anchor regexes to field names so repeated CI runs don’t double-apply. |
| Raise to Perl/awk/yq | Use Perl for multiline, awk for structured data, yq/jq for YAML/JSON. |
How sed replace syntax actually works
The s command is the workhorse of sed find and replace. Its full form is:
s/REGEXP/REPLACEMENT/FLAGS
Any single character can serve as the delimiter. The slash is conventional, but |, :, and # all work. That matters when your pattern contains slashes.
Special replacement tokens:
&expands to the entire matched string\1through\9reference capture groups in the order they open- To use a literal
&in the replacement, escape it as\& - To use a literal backslash, write
\\
Common flags (per the GNU sed manual):
| Flag | Meaning | Portability |
|---|---|---|
g |
Replace all matches on the line | POSIX — works everywhere |
NUMBER |
Replace only the Nth match | POSIX |
p |
Print the line when a substitution occurs | POSIX |
I |
Case-insensitive match | GNU only |
i |
Case-insensitive (lowercase) | GNU only |
The I flag is a GNU extension. BSD sed on macOS does not support it. Use a character-class workaround ([Ff][Oo][Oo]) when you need case-insensitive matching on both platforms.
Copy-ready examples you can paste right now
These are the patterns you’ll reach for most often. Each one runs as-is in your terminal.
Replace the first match on each line:
sed 's/foo/bar/' file.txt
Without the g flag, sed stops after the first match per line.
Replace every match on every line:
sed 's/foo/bar/g' file.txt
Replace only the second match on each line:
sed 's/foo/bar/2' file.txt
Case-insensitive replacement (GNU only):
sed 's/foo/bar/gI' file.txt
BSD-compatible case-insensitive fallback:
sed 's/[Ff][Oo][Oo]/bar/g' file.txt
Alternate delimiter for paths (avoids escaping every /):
sed 's|/usr/local|/opt|g' file.txt
The LinuxCapable sed guide recommends alternate delimiters any time the pattern contains slashes. It keeps the command readable.
Escaping a literal & in the replacement:
sed 's/price/\& (USD)/g' file.txt
Without the backslash, & would expand to price and you’d get price (USD) — which is actually correct here. Escape it when you want the ampersand character itself.
Pro Tip: Use sed -n 's/foo/bar/gp' file.txt to print only the lines that changed. Fast way to confirm your pattern hits what you expect.
Editing files in place: GNU vs BSD/macOS gotchas
This is where most cross-platform scripts break. The -i flag behaves differently between GNU sed (Linux) and BSD sed (macOS).
GNU sed accepts -i with an optional suffix. No suffix means no backup:
sed -i 's/foo/bar/g' file.txt # GNU: no backup
sed -i.bak 's/foo/bar/g' file.txt # GNU: backup as file.txt.bak
BSD sed (macOS) requires the suffix as a separate argument. Omitting it causes a parse error where sed misreads your script string as the backup suffix:
sed -i '' 's/foo/bar/g' file.txt # BSD: no backup
sed -i .bak 's/foo/bar/g' file.txt # BSD: backup (note the space)
The GNU vs BSD sed breakdown from Karandeep Singh documents exactly this failure mode. Running sed -i 's/foo/bar/g' file.txt on macOS silently treats s/foo/bar/g as the backup suffix and writes nothing useful.
Portable pattern that works on both:
sed -i.bak 's/foo/bar/g' file.txt && rm file.bak
Atomic temp-file pattern (production-safe):
sed 's/foo/bar/g' file.txt > file.tmp && mv file.tmp file.txt
| Platform | -i alone |
-i '' |
-i.bak |
Temp + mv |
|---|---|---|---|---|
| GNU/Linux | Works (no backup) | Syntax error | Works | Works |
| macOS/BSD | Breaks silently | Works (no backup) | Works | Works |
| CI/CD scripts | Risky | Platform-specific | Safe | Safest |
Pro Tip: In any shared script or CI job, default to -i.bak and clean up the backup after validation. It costs one extra rm and saves hours of debugging on macOS runners.
Capture groups, backreferences, and word boundaries
Capture groups let you rearrange matched text in the replacement. In Basic Regular Expressions (BRE, the default), groups use escaped parentheses:
sed 's/\(first\) \(last\)/\2 \1/' file.txt
With -E (Extended Regular Expressions), the escaping goes away:
sed -E 's/(first) (last)/\2 \1/' file.txt
Use -E whenever your pattern has multiple groups. It cuts visual noise significantly. Note that GNU sed also accepts -r as an alias for -E, but -E is the POSIX-standard flag and works on both platforms.
Word boundaries are where portability gets tricky. GNU sed supports \b:
sed 's/\bfoo\b/bar/g' file.txt # GNU only
BSD sed does not recognize \b. Use POSIX bracket expressions instead:
sed 's/[[:<:]]foo[[:>:]]/bar/g' file.txt # BSD/macOS
For scripts that must run on both, the safest approach is to write more specific patterns (anchors, surrounding characters) rather than relying on \b or its BSD equivalent. The Baeldung GNU vs BSD comparison notes that when portability is the priority, awk or Perl offer more consistent regex behavior across systems.
Running sed across many files safely
Batch replacements need a precheck step. Skipping it risks touching binary files, .git internals, or files that don’t need the change.
Here’s a safe numbered workflow:
- Find files containing the match (text files only, null-delimited for xargs safety):
grep -rIlZ 'foo' ./src - Preview the substitution without writing:
grep -rIlZ 'foo' ./src | xargs -0 sed 's/foo/bar/g' - Apply with backups once the preview looks right:
grep -rIlZ 'foo' ./src | xargs -0 sed -i.bak 's/foo/bar/g' - Remove backups after confirming the result:
find ./src -name '*.bak' -delete
The -I flag in grep skips binary files. The -Z flag outputs null-terminated filenames, which xargs -0 handles correctly even when filenames contain spaces.
A find-based alternative for excluding .git directories:
find ./src -type f -name '*.txt' ! -path '*/.git/*' \
-exec sed -i.bak 's/foo/bar/g' {} +
The LinuxCapable guide recommends this dry-run-first approach for any recursive replacement. One preview step prevents a lot of cleanup.

Troubleshooting common sed mistakes
Problem → Fix pairs for the errors you’ll actually hit:
- “extra characters after command” on macOS: You ran
sed -i 's/foo/bar/g' file. BSD sed reads/foo/bar/gas the backup suffix. Fix: usesed -i.bakorsed -i ''. - Replacement replaces too much (partial match): Your pattern matches substrings. Add anchors (
^,$) or word boundaries.s/log/LOG/galso hitslogging. Uses/\blog\b/LOG/gon GNU or the[[:<:]]form on BSD. &appears literally in output instead of the match: You forgot to escape it.\&gives you a literal ampersand.- “illegal byte sequence” on macOS: BSD sed chokes on non-ASCII bytes in UTF-8 files. Set
LC_ALL=Cbefore the command:LC_ALL=C sed -i.bak 's/foo/bar/g' file.txt. This treats the file as raw bytes and skips encoding validation. The unix.stackexchange thread on macOS sed differences documents this as one of the most common macOS-specific failures. \bnot recognized on macOS: GNU-only escape. Switch to[[:<:]]foo[[:>:]]or use Perl:perl -pi -e 's/\bfoo\b/bar/g' file.txt.-Evs-rconfusion: Both enable extended regex, but-ris GNU-only. Use-Eeverywhere.
Pro Tip: Before any in-place edit, run the command without -i and pipe to diff - file.txt to see exactly what changes. Zero surprises.
Multiline replacements and when to drop sed entirely
Sed processes one line at a time. Its pattern space holds a single line by default, so cross-line matching requires explicit tricks.
The N command appends the next line to the pattern space with a newline separator:
sed 'N; s/foo
bar/baz/' file.txt
This works for two-line sequences but gets fragile fast with longer spans. GNU sed adds -z (null-delimited mode), which treats the entire file as one record:
sed -z 's/foo
bar/baz/g' file.txt # GNU only
BSD sed has no -z. For anything involving multiline patterns on both platforms, Perl is the cleaner choice:
perl -0777 -pi -e 's/foo
bar/baz/g' file.txt
The Advanced Bash-Scripting Guide covers multi-command sed scripts in depth, but even it acknowledges the limits of the pattern-space model.
For structured files (YAML, JSON, TOML), skip sed entirely. yq for YAML and jq for JSON parse the structure rather than treating it as raw text. One misplaced sed replacement in a YAML config can corrupt indentation and break the whole file silently.
Pro Tip: If your sed command needs more than two N commands or a H/G cycle to work, rewrite it in Perl. The time you save debugging is worth it.
CI/CD and pipeline-safe sed patterns
Sed in automated pipelines needs four hardening rules. Karandeep Singh frames sed as a scalpel: powerful, but risky when run repeatedly without guards.
Here’s a numbered checklist for pipeline authors:
- Precheck before mutating. Run
grep -q 'old_value' filefirst. If it exits non-zero, skip the sed call. This prevents double-application and gives clear failure signals when schemas change. - Anchor your regex to a key. Instead of
s/2\.0/3\.0/g, writes/^\(version:\s*\).*/\1 3.0/so the pattern only fires on the right field. - Emit a diff before writing. Run the substitution without
-i, capture the output, and logdiff -u file.txt <(sed 's/.../.../' file.txt)to your CI audit log. - Use
-i.bakor temp-file + mv, then validate before removing the backup. The-iflag is not atomic on many systems: sed writes to a temp file and renames it, which can expose partial state and change the file’s inode. The temp-file + mv pattern is more explicit about that trade-off.
Safe shell wrapper (dry-run by default):
#!/bin/sh
FILE="$1"
PATTERN="$2"
REPLACEMENT="$3"
if ! grep -q "$PATTERN" "$FILE"; then
echo "Pattern not found. No changes made."
exit 0
fi
if [ "${APPLY:-}" = "1" ]; then
sed -i.bak "s/$PATTERN/$REPLACEMENT/g" "$FILE" && rm "${FILE}.bak"
echo "Applied."
else
sed "s/$PATTERN/$REPLACEMENT/g" "$FILE" | diff - "$FILE" || true
echo "Dry run. Set APPLY=1 to mutate."
fi
Run as ./replace.sh config.yml old new for a preview, then APPLY=1 ./replace.sh config.yml old new to commit. This pairs well with workflow automation patterns that need safe, repeatable text mutations across config files.
For content pipelines where sed touches marketing copy or CMS exports, an automated publishing checklist helps ensure text edits don’t corrupt downstream formatting.
Pro Tip: Add a rollback hook: if the post-edit validation step fails, mv file.bak file restores the original. One line of shell, zero regrets.
Quick-reference cheatsheet for common sed tasks
Copy these.
- First match only:
sed 's/foo/bar/' file.txt - All matches (global):
sed 's/foo/bar/g' file.txt - Case-insensitive (GNU):
sed 's/foo/bar/gI' file.txt - Case-insensitive (BSD-safe):
sed 's/[Ff][Oo][Oo]/bar/g' file.txt - Alternate delimiter for paths:
sed 's|/old/path|/new/path|g' file.txt - Whole-word match (GNU):
sed 's/\bfoo\b/bar/g' file.txt - Whole-word match (BSD):
sed 's/[[:<:]]foo[[:>:]]/bar/g' file.txt - In-place with backup (portable):
sed -i.bak 's/foo/bar/g' file.txt - In-place no backup (macOS):
sed -i '' 's/foo/bar/g' file.txt - Atomic temp-file write:
sed 's/foo/bar/g' file.txt > file.tmp && mv file.tmp file.txt - Recursive (grep + xargs):
grep -rIlZ 'foo' . | xargs -0 sed -i.bak 's/foo/bar/g' - Print only changed lines:
sed -n 's/foo/bar/gp' file.txt - Nth match only:
sed 's/foo/bar/2' file.txt - Capture group reorder:
sed -E 's/(first) (last)/\2 \1/' file.txt - Fix encoding errors (macOS):
LC_ALL=C sed -i.bak 's/foo/bar/g' file.txt
| Task | Command pattern | Notes |
|---|---|---|
| Global replace | s/old/new/g |
Add g flag |
| Path strings | s|/old|/new|g |
Alternate delimiter |
| In-place portable | -i.bak ... && rm *.bak |
Works GNU + BSD |
| Recursive files | grep -rIlZ | xargs -0 sed |
Skip binaries with -I |
| Multiline (GNU) | `sed -z 's/a | |
| b/c/g’` | GNU only; use Perl for portability |
Dry-run before -i. Use -i.bak for portability. Use temp-file + mv in production scripts where atomicity matters.
Why Rule27design prefers guarded sed in production
Sed is fast and available everywhere. That’s exactly why it’s dangerous in automated systems. A single unguarded sed -i in a deploy script can silently corrupt a config file across every environment it touches, with no rollback and no log entry.
At Rule27design, the default is: no -i without a backup, no batch replacement without a precheck, and no pipeline step without a diff in the audit log. These aren’t extra steps. They’re the minimum for a script you’d trust to run at 2 AM without supervision.
Test commands in a disposable workspace or a CI dry-run job first. The wrapper in the pipeline-safety section above is a good starting point for any team that wants repeatable, observable text mutations.
Sources
About the Author
Josh AndersonCo-Founder & CEO at Rule27 Design
Operations leader and full-stack developer with 15 years of experience disrupting traditional business models. I don't just strategize, I build. From architecting operational transformations to coding the platforms that enable them, I deliver end-to-end solutions that drive real impact. My rare combination of technical expertise and strategic vision allows me to identify inefficiencies, design streamlined processes, and personally develop the technology that brings innovation to life.
View Profile


