Text surgery: find and replace across a codebase

Drive editor and command-line replacements with capture groups, preview the diff before writing, and reduce the risk of a bulk edit.

Editor find and replace

ToolBackreference in the replacementNotes
VS Code$1 or ${1}regex mode toggle on; $0 is the whole match
JetBrains IDEs$1backslash escapes in the search field
vim\1:%s/pat/repl/gc with confirmation
Emacs\1M-x query-replace-regexp
sed\1BRE by default, use -E for ERE
# a task: rename get_user to fetch_user, including call sites
find:    \bget_user\b
replace: fetch_user

# a task: turn obj.prop access into a destructured constant
find:    const (\w+) = (\w+)\.(\w+);
replace: const $1 = $2.$3;   ->   run a second pass if you want destructuring

# a task: add a trailing comma to single-line array literals
find:    \[(\s*[^,\[\]]+)\]            # risky, review the preview
replace: [$1,]

# a task: wrap a literal in a helper, keeping the original text
find:    log\((['"])(.*?)\1\)
replace: logger.info($1$2$1)
  • Turn on whole-word matching rather than typing \b everywhere. It is the same assertion with far less to get wrong.
  • Case sensitivity is a separate toggle and it defaults to sensitive in some tools and insensitive in others. Check it explicitly before running.
  • Scope the search to a folder or a file pattern. A repository-wide replacement that also rewrites vendored dependencies is very hard to unpick.

ripgrep and sed

# find first, replace never: rg prints a preview and writes nothing
rg -n --glob '!node_modules' '\bget_user\b'

# include the surrounding context to check the shape of the call sites
rg -n -C2 '\bget_user\b' src/

# rg --replace only rewrites its own output; it does not touch files
rg -n -r 'fetch_user' '\bget_user\b' src/

# preview the real edit with sed, without -i
grep -rl --include='*.js' '\bget_user\b' src/ \
  | xargs sed -E 's/\bget_user\b/fetch_user/g' \
  | diff - <(cat)            # or simply review the printed output

# apply, after reviewing
grep -rl --include='*.js' '\bget_user\b' src/ \
  | xargs sed -E -i 's/\bget_user\b/fetch_user/g'
  • grep -rl lists the files that would change. Run it before any write and compare the list with what you expect.
  • sed -i on macOS needs an explicit empty suffix: sed -i '' -E .... On GNU sed it takes no argument.
  • Keep the change set small: one rename per commit. A combined edit that changes identifiers and formatting at once cannot be reviewed, and cannot be reverted in part.
  • Almost every language has a real refactoring tool. When the change is a symbol rename, the language server is safer than any regex.

Reviewing the edit

The preview is the whole method. Regex replacement fails quietly: a pattern that over-matches produces valid-looking code that compiles and misbehaves later. Make the diff cheap to read before you make it large.

git add -A                     # stage everything, then inspect
git diff --cached --stat       # how many files and how many lines
git diff --cached              # read it, do not skim it

git diff --cached -G'fetch_user'      # only hunks touching the new name
git diff --cached | grep -c '^+'      # count added lines as a sanity check

git checkout -- .              # abandon the whole edit if the diff is wrong
⚠️
Never run a bulk replacement with uncommitted work in the tree. Without a clean baseline you cannot tell which lines the pattern changed and which were already modified, and git checkout is no longer a safe undo.

FAQ

Why did my replacement insert a literal $1?
The search pattern had no capture group, or the capture group was written as non-capturing. Editors and sed differ too: sed uses backslash references, most editors use dollar references. Check the replacement syntax of the specific tool.
How do I replace only inside a specific function?
Regex cannot see nesting, so restrict the search to a file range, select the region first, or use a structured tool. Editing one selected block in the editor is faster and safer than building a pattern that tries to understand scope.

Regex in JavaScript When not to use regex

Last refreshed 2026-09-18.