Toolcedar

Regex Find and Replace: Five Patterns Worth Learning

Capture groups, anchors, lazy quantifiers and character classes cover almost every real find-and-replace job. Here is each one, with the traps.

· 3 min read

Most people learn regular expressions by collecting incantations. That works badly, because a pattern copied from an answer online is unreadable the moment it needs a small change. Five ideas cover the overwhelming majority of real find-and-replace work, and knowing them by name is enough to write and modify patterns with confidence.

1. Capture groups: keeping what you matched

Anything in parentheses is captured and can be referenced in the replacement, usually as $1, $2 and so on. This is what turns search-and-destroy into genuine restructuring: you match a shape, keep the parts you want, and rewrite the rest around them.

find:     (\w+), (\w+)
replace:  $2 $1

"Lovelace, Ada"  ->  "Ada Lovelace"

Named groups make longer patterns readable — (?<year>\d{4}) can be referenced as $<year> rather than by counting parentheses. Once a pattern has more than two groups, counting is where mistakes come from, so naming them is worth the extra characters.

2. Character classes: matching a kind of thing

Square brackets match any one of the characters inside them, and a leading caret negates the set. The shorthands are worth memorising because they appear everywhere: \d is a digit, \w is a word character, \s is whitespace, and each has an uppercase form meaning the opposite.

The trap is that \w means letters, digits and underscore in the ASCII sense. It does not match accented letters unless the pattern opts into Unicode handling, so a naive word-matching pattern quietly skips half the names in the world.

3. Greedy versus lazy: the classic bug

Quantifiers are greedy by default: they take as much as they possibly can and then give characters back only if the rest of the pattern fails. That is almost never what you want when matching something delimited.

input:   <b>bold</b> and <i>italic</i>

<.+>     matches the entire line — greedy
<.+?>    matches <b>, then </b>, then <i>… — lazy

Adding a question mark after a quantifier makes it lazy: take as little as possible. If a replacement is swallowing everything between the first and last delimiter on a line, greediness is the reason, and the question mark is the fix.

4. Anchors and boundaries: matching in the right place

A caret matches the start of the input, a dollar the end, and in multiline mode each matches the start and end of every line. Word boundaries — \b — are the ones that save the most time in practice: searching for "cat" finds it inside "concatenate", and searching for \bcat\b does not.

Almost every accidental replacement is a missing word boundary. Add \b on both sides before blaming the pattern.

5. Escaping: when a character means itself

A dot matches any character, so a pattern searching for a literal full stop finds every character in the file. The same applies to the other metacharacters: * + ? ( ) [ ] { } ^ $ | \ and the dot itself all need a backslash when you mean them literally. If a pattern is matching far more than expected, an unescaped dot is the first thing to check.

What not to use it for

Regular expressions cannot parse nested structures. HTML, JSON, XML and source code all allow arbitrary nesting, and no regular expression can track how deep it currently is. A pattern that appears to work on your sample will fail on the first nested case, and the failure is usually silent corruption rather than an error.

Use a parser for structure, and regular expressions for the flat text inside it. The other caution is performance: patterns combining nested quantifiers, such as (a+)+, can take exponential time on input that nearly matches. On text you control this is a curiosity; on user input it is a denial-of-service vector with a name — catastrophic backtracking.

Frequently asked questions

Why does my replacement affect words I did not intend?
Almost always a missing word boundary. Without \b on each side, a search for a short word matches it inside longer words too. Adding boundaries fixes it without changing anything else about the pattern.
What is the difference between greedy and lazy matching?
A greedy quantifier takes as many characters as it can and backtracks only if the rest of the pattern fails; a lazy one, written with a trailing question mark, takes as few as possible. Use lazy matching for anything between delimiters.
Can I use a regular expression to extract data from HTML?
Only for flat, predictable fragments. HTML allows arbitrary nesting and a regular expression cannot track nesting depth, so any pattern will break on a structure slightly different from your sample. Use a real parser for anything that matters.