Regex Find and Replace

Find-and-replace is where regex stops being a search tool and starts being a transformation tool. The trick is capture groups — pieces of the match you can pull into the replacement, in any order, with any surrounding text. This guide covers the syntax (which is annoyingly different in every engine), working examples in JavaScript, Python, sed, and vim, and the practical tasks that come up most: reformatting dates, building markdown links, renaming variables, and stripping tracking parameters. Test patterns in the regex tester before running them on real files.

The Basic Idea

Regex find-and-replace has two parts: a pattern with capture groups, and a replacement string that references those groups. Wrap each piece you want to reuse in parentheses, then refer to it in the replacement by number.

# Reformat YYYY-MM-DD as MM/DD/YYYY
Pattern:     (\d{4})-(\d{2})-(\d{2})
Replacement: $2/$3/$1   (or \2/\3/\1 in Python, sed, vim)
Input:       2024-03-10
Output:      03/10/2024

The numbered backreferences ($1, $2, $3) point to the first, second, and third parenthesized groups in the pattern. You can use them in any order, repeat them, or skip them entirely.

$1 vs \1 — Engine Differences

The replacement-string syntax is one of the most fragmented parts of regex. Every engine settled on its own convention:

Engine            Backreference   Whole match   Named group
JavaScript        $1              $&            $<name>
Python (re)       \1              \g<0>         \g<name>
sed (POSIX/GNU)   \1              &             (no named groups)
vim               \1              &             (no named groups)
Perl              $1              $&            $+{name}
.NET              $1              $0            ${name}
PCRE / Java       $1              $0            ${name}

Three rules of thumb:

  • If you're in JavaScript, .NET, Java, or modern PCRE — use $1, $2, etc.
  • If you're in Python, vim, sed, or Perl — use \1, \2, etc.
  • Test in the right environment. A pattern that works in JavaScript may insert literal $1 in Python.

JavaScript

// Replace one match
"hello world".replace(/world/, "there");
// "hello there"

// Replace all matches — the g flag is essential
"hello world".replace(/o/g, "0");
// "hell0 w0rld"

// Replace with capture groups
"2024-03-10".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");
// "03/10/2024"

// Named capture groups (modern engines)
"2024-03-10".replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  "$<m>/$<d>/$<y>"
);
// "03/10/2024"

// Function as replacement — for conditional or computed replacements
"abc 123 def 456".replace(/\d+/g, m => parseInt(m, 10) * 2);
// "abc 246 def 912"

// Function with named groups
"alice@a.com bob@b.com".replace(
  /(?<user>\w+)@(?<dom>[\w.]+)/g,
  (...args) => {
    const groups = args[args.length - 1];
    return `${groups.user} [at] ${groups.dom}`;
  }
);
// "alice [at] a.com bob [at] b.com"

// replaceAll — explicit alternative; requires g flag with regex
"a-b-c".replaceAll(/-/g, "_");
// "a_b_c"

replace and replaceAll never mutate the original string — they always return a new one. See the JavaScript regex guide for the difference between replace and replaceAll, and how to avoid common lastIndex bugs with the g flag.

Python

import re

# Replace all matches
re.sub(r"o", "0", "hello world")
# 'hell0 w0rld'

# Replace with capture groups (use \1, \2, etc.)
re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", "2024-03-10")
# '03/10/2024'

# Named capture groups — use \g<name>
re.sub(
    r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})",
    r"\g<m>/\g<d>/\g<y>",
    "2024-03-10"
)
# '03/10/2024'

# Function as replacement
re.sub(r"\d+", lambda m: str(int(m.group(0)) * 2), "abc 123 def 456")
# 'abc 246 def 912'

# Limit number of replacements with count
re.sub(r"o", "0", "hello world", count=1)
# 'hell0 world'

# subn returns (new_string, count) — useful for "did anything change?"
new, n = re.subn(r"foo", "bar", text)
if n:
    print(f"Replaced {n} matches")

Always use raw strings (r"...") for both the pattern and the replacement — without raw strings, \1 becomes the SOH control character. re.compile the pattern if you'll reuse it. See the Python regex guide for more on re.sub, subn, and verbose patterns.

sed

# Replace first match per line
sed "s/foo/bar/" file.txt

# Replace all matches per line (g flag)
sed "s/foo/bar/g" file.txt

# Use extended regex (-E) for cleaner syntax
sed -E "s/(foo|bar)/baz/g" file.txt

# Capture groups with backreferences
sed -E "s/(\w+)@(\w+\.\w+)/\1 [at] \2/g" emails.txt

# Edit a file in place
# Linux / GNU sed:
sed -i "s/foo/bar/g" file.txt
# macOS / BSD sed (note the required empty backup arg):
sed -i "" "s/foo/bar/g" file.txt

# Multiple substitutions in one command
sed -E -e "s/foo/bar/g" -e "s/baz/qux/g" file.txt

# Case-insensitive (GNU extension)
sed "s/foo/bar/gi" file.txt

# Use a different delimiter when the pattern has many slashes
sed "s|/usr/local/bin|/opt/bin|g" config

Without -E (or -r on GNU sed), you have to escape (, ), +, ?, and | with backslashes — almost always easier to use -E. The whole match is referenced as &; capture groups are \1 through \9. macOS ships BSD sed with subtly different in-place semantics from GNU sed — write a 5-line Python script if you need cross-platform reliability.

vim

" Replace all in current file (% means whole file, g means all per line)
:%s/foo/bar/g

" Replace with capture groups (vim uses \1, \2, etc.)
:%s/\(\d\{4}\)-\(\d\{2}\)-\(\d\{2}\)/\2\/\3\/\1/g

" Use \v ("very magic") to avoid escaping (, ), +, ?
:%s/\v(\d{4})-(\d{2})-(\d{2})/\2\/\3\/\1/g

" Confirm each replacement (c flag)
:%s/foo/bar/gc

" Case-insensitive (i flag)
:%s/foo/bar/gi

" Replace only inside a visual selection
:'<,'>s/foo/bar/g

" Replace across all open buffers (:bufdo) or all files in args (:argdo)
:bufdo %s/foo/bar/ge | update
:argdo %s/foo/bar/ge | update

Vim's regex syntax is its own dialect — by default, (, ), +, and ? are literal characters and need escaping. \v ("very magic") at the start of the pattern switches to PCRE-like syntax where they're metacharacters. The e flag (in :bufdo / :argdo) suppresses errors when a buffer doesn't contain the pattern.

Practical Examples

Reformat dates

# YYYY-MM-DD → MM/DD/YYYY
Pattern:     (\d{4})-(\d{2})-(\d{2})
Replacement: $2/$3/$1   (JS)  or  \2/\3/\1   (Python, sed, vim)

# DD.MM.YYYY → YYYY-MM-DD
Pattern:     (\d{2})\.(\d{2})\.(\d{4})
Replacement: $3-$2-$1

Wrap URLs in markdown links

# Plain URL → [link text](url)
Pattern:     (https?:\/\/\S+)
Replacement: [link]($1)

# JavaScript:
text.replace(/(https?:\/\/\S+)/g, "[link]($1)");

Convert camelCase to snake_case

# Insert _ before each uppercase letter, then lowercase
# JavaScript:
"camelCaseExample".replace(/([A-Z])/g, "_$1").toLowerCase();
// "camel_case_example"

# Python:
import re
re.sub(r"([A-Z])", r"_\1", "camelCaseExample").lower()
# 'camel_case_example'

The case converter handles this conversion (and the reverse) without any code.

Strip URL tracking parameters

# Remove utm_source, utm_medium, utm_campaign, etc. from query strings
Pattern:     [?&]utm_\w+=[^&]*
Replacement: (empty)

# Then collapse a leading & or ? followed by nothing:
Pattern:     \?(?:&|$)
Replacement: ""

Rename a function across a codebase (with sed)

# Rename oldName to newName, but only as a whole word
find . -name "*.js" -exec sed -i "" -E "s/\boldName\b/newName/g" {} +

# Linux / GNU sed (no empty backup arg):
find . -name "*.js" -exec sed -i -E "s/\boldName\b/newName/g" {} +

The \b word boundary prevents matches inside oldNameSuffix or prefixOldName. For multi-line refactors, use a real refactoring tool (your IDE, jscodeshift, ast-grep) instead of regex — it understands scope and won't rename a string literal that happens to contain the word.

Common Edge Cases

Replacement strings that contain special characters

If your replacement contains $ (JavaScript), \, or & (sed), they may be interpreted as backreferences or the whole-match marker. To insert a literal $ in JavaScript, use $$. In sed, escape & as \&. In Python, escape \ as \\.

Replacement that depends on the match value

When the replacement isn't a fixed template — for example, you want to double every number — pass a function instead of a string. JavaScript's str.replace(regex, fn) and Python's re.sub(pat, fn, text) both call your function once per match with the match object, and use the return value as the replacement. Faster and clearer than parsing the result of a regular replace and post-processing.

Matching across newlines

By default, . doesn't match newlines. To match across lines, use the s (dotall) flag — supported in JavaScript ES2018+, Python (re.DOTALL), and most modern engines. Sed treats input one line at a time by default; use the N command to pull the next line into the pattern space, or pre-process with tr "\n" "\0" to swap newlines for nulls.

Greedy vs lazy quantifiers

.* is greedy — it matches as much as possible. In <b>one</b> and <b>two</b>, the pattern <b>.*</b> matches the whole string from the first <b> to the last </b>. Use .*? (lazy) to match the minimum: <b>.*?</b> matches <b>one</b> and then <b>two</b> separately. This is the single most common find-and-replace bug.

Try It Live

Build and test your replacement in the regex tester first — it shows the matches highlighted and runs entirely in your browser, so you can paste sensitive data without it leaving your machine. For an explanation of any pattern token-by-token, the regex explainer breaks it down. The pattern library has copy-ready patterns for the common cases (dates, emails, URLs, IPs).

Frequently Asked Questions

How do I use capture groups in regex find-and-replace?

Wrap the part of the pattern you want to reuse in parentheses — that creates a capture group. In the replacement string, refer to each captured group by its number: $1 in JavaScript, sed, and most modern engines; \1 in Python, vim, and older sed. The first parenthesized group is $1 (or \1), the second is $2, and so on. To reformat 2024-03-10 as 03/10/2024 in JavaScript, use "2024-03-10".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1"). The same logic works in any engine — just swap the backreference syntax.

What is the difference between $1 and \1 in replacement strings?

They mean the same thing — a reference to a capture group — but different engines accept different syntax. JavaScript, .NET, Java, and modern PCRE use $1, $2, etc. in replacement strings. Python, vim, sed (BSD and traditional GNU), and Perl use \1, \2, etc. JavaScript also accepts $& for the entire match. Python uses \g for named groups; JavaScript uses $. Always check your engine's docs before assuming — running a Python pattern with $1 will literally insert $1 into the replacement.

How do I do regex find-and-replace in JavaScript?

Use str.replace(regex, replacement) to replace the first match, or pass a global regex (/.../ + g flag) to replace all matches: "hello world".replace(/o/g, "0") gives "hell0 w0rld". The replacement string can include $1, $2, etc. for capture groups, or you can pass a function that receives the match and groups and returns the replacement string. str.replaceAll(regex, replacement) is the explicit alternative for replacing all matches; it requires the g flag if the first argument is a regex.

How do I do regex find-and-replace with sed?

The basic syntax is sed -E "s/pattern/replacement/g" file. The s/// command means substitute; the trailing g flag replaces all matches on each line (without g, only the first per line). The -E flag enables extended regex (POSIX ERE), which gives you +, ?, |, and unescaped parentheses for grouping — without -E you have to write \(, \), \+, etc. To edit a file in place: sed -i "s/foo/bar/g" file (Linux/GNU) or sed -i "" "s/foo/bar/g" file (macOS/BSD; the empty string is a required backup-file argument).

How can I use a function as the replacement instead of a string?

In JavaScript, str.replace(regex, fn) calls fn for each match and uses the return value as the replacement. The function receives the full match, then each capture group, then the offset and original string. This lets you do conditional replacements, lookups, or computations: "abc 123".replace(/\d+/g, m => parseInt(m, 10) * 2) gives "abc 246". In Python, re.sub(pattern, fn, text) works the same way; fn receives a Match object and returns a string. This is the right approach when the replacement depends on the value of the match — for anything beyond simple template substitution.