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
| Tool | Backreference in the replacement | Notes |
|---|---|---|
| VS Code | $1 or ${1} | regex mode toggle on; $0 is the whole match |
| JetBrains IDEs | $1 | backslash escapes in the search field |
| vim | \1 | :%s/pat/repl/gc with confirmation |
| Emacs | \1 | M-x query-replace-regexp |
| sed | \1 | BRE 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
\beverywhere. 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 -rllists the files that would change. Run it before any write and compare the list with what you expect.sed -ion 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.
Related
Regex in JavaScript When not to use regex
Last refreshed 2026-09-18.