Replace a string across multiple files
Apply the same find-and-replace operation to a batch of files (filtered by glob, by content, or by recursion) — for renaming an API, fixing a typo across a codebase, or updating a config value.
How to replace a string across multiple files in each shell
grep -rl "old-string" . | xargs sed -i 's/old-string/new-string/g'`grep -rl` (recursive, list filenames only) finds files containing the string; piping through `xargs sed -i` does the replace ONLY on matching files (much faster on big trees than `sed -i ... **/*`). Pre-flight with the dry-run: `grep -rl "old-string" .` first — verify the list of files looks right before adding `sed -i`. On macOS BSD `sed`, the in-place flag is `-i ''` (empty backup suffix); GNU `sed` accepts bare `-i`. The portable form is `sed -i.bak 's/old/new/g' && rm *.bak`.
grep -rl "old-string" . | xargs sed -i 's/old-string/new-string/g'Same external. Modern alternative: `rg --files-with-matches "old-string" | xargs sed -i 's/old-string/new-string/g'` — ripgrep respects `.gitignore` and is faster on large trees. For "in-tool" replace with ripgrep: `sd 'old-string' 'new-string' $(rg -l old-string)` (`sd` is a friendlier sed alternative, `brew install sd`).
grep -rl "old-string" . | xargs sed -i 's/old-string/new-string/g'Same external. For fish-native dry-run loop: `for f in (grep -rl "old-string" .); echo $f; end`. Then once verified, run the sed. Fish `string replace` is good for one variable but doesn't do in-place file edits — pipe through it for stdin/stdout: `cat file.txt | string replace -a "old-string" "new-string" > file.txt.new && mv file.txt.new file.txt`.
Get-ChildItem -Recurse -File | ForEach-Object { $c = Get-Content $_ -Raw; if ($c -match 'old-string') { ($c -replace 'old-string','new-string') | Set-Content $_ -NoNewline } }Reads each file once via `Get-Content -Raw` (whole file as a single string — multi-line patterns work). Only writes if the pattern was found (avoids touching mtime on unrelated files). `-NoNewline` preserves the original trailing-newline behaviour (without it, `Set-Content` adds a newline that may not have been there). `-replace` is REGEX by default — escape literal special characters with `[regex]::Escape('old-string')`. Encoding: defaults to UTF-8-BOM on Windows PS 5.1 — pwsh 7 + `-Encoding utf8NoBOM` is the modern default.
powershell -NoProfile -Command "Get-ChildItem -Recurse -File | ForEach-Object { $c = Get-Content $_ -Raw; if ($c -match 'old-string') { ($c -replace 'old-string','new-string') | Set-Content $_ -NoNewline } }"cmd has no in-place file edit. Shell out to pwsh. For very-simple "replace literal string, no regex", install GNU `sed` via Scoop (`scoop install sed`) and the bash one-liner works under cmd too.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- The single biggest footgun: forgetting that `sed -i` rewrites EVERY matched file even if no actual replacement happened (because regex matched zero occurrences) — touching mtime, busting build incremental cache, generating spurious git modifications. The `grep -rl | xargs sed` pattern avoids this by only invoking sed on files KNOWN to contain the pattern. The pwsh form above similarly skips writes if `-match` fails.
- For SAFE regex replace, escape metacharacters in the source pattern. bash: `sed -i "s/$(printf '%s' "old.string" | sed 's/[[\.*^$()+?{|]/\\&/g')/new-string/g"`. pwsh: `$pattern = [regex]::Escape("old.string"); $c -replace $pattern, "new-string"`. Literal-string-replace tools (`sd`, `perl -pe 's/\Qold\E/new/g'`) avoid the escape dance.
- Always run on a clean git working tree (so the diff = your replace + nothing else). Run dry-run first (`grep -rl` alone, pwsh without `Set-Content`, sd `-p` preview flag). Then commit the replace in isolation. This lets you `git diff` post-replace and verify the substitution semantics matched intent before merging.
- For LARGE binary-ish replacements (e.g. embedding a hash, replacing a UUID, swapping a multi-line YAML block), prefer scripting language with proper escape handling: Python `pathlib.Path(f).write_text(pathlib.Path(f).read_text().replace("old","new"))`. Shell tools optimize for line-oriented text — multi-line replacement in `sed` requires `N` hold-space gymnastics that are easy to get wrong.
Related commands
Related tasks
- Find and replace text in files— Substitute one string for another inside a file (or every file in a tree), in place.
- Grep recursively with context lines— Search a directory tree for a pattern and print N lines of surrounding context for each match — for code archaeology, log spelunking, or config-file forensics.