Skip to main content
PocketToolz

Regular expressions, from the parts you actually need

The dozen or so pieces that cover almost every real use, why greedy matching surprises everyone, and when not to reach for a regex at all.

9 min read

Regular expressions have a reputation for being unreadable, and the reputation is deserved for the elaborate ones. But almost every real use needs about a dozen pieces, and those dozen are worth knowing properly rather than pasting from an answer you do not understand.

The pieces that cover most of it

PatternMatchesExample
.Any single charactera.c → abc, a7c
\d \w \sDigit, word character, whitespace\d\d → 42
[abc]Any one of these[aeiou] → a vowel
[^abc]Anything except these[^0-9] → not a digit
* + ?None or more, one or more, optionalcolou?r → color, colour
{2,4}Between two and four\d{4} → a year
^ $Start and end of the string^abc$ → exactly abc
|Either sidecat|dog
(…)Group, and capture it(\d+)-(\d+)
\bA word boundary\bcat\b → not concatenate

That is genuinely most of it. The capital versions of the shorthands invert them — \D is any non-digit, \S any non-whitespace — which is two more for free.

Greedy matching, the thing that surprises everyone

Quantifiers are greedy: they take as much as they can and only give back what they must. This is the single most common source of a regex that looks right and behaves wrongly.

Given <b>bold</b> and <i>italic</i>, the pattern <.*> does not match <b>. It matches the entire string, from the first < to the last >, because .* grabs everything and then backtracks only far enough to find a final >.

Add ? to make a quantifier lazy — it then takes as little as possible. <.*?> matches <b>, as intended.

Often better still: stop it crossing the boundary at all. <[^>]*> says “anything that is not a closing bracket”, which cannot overshoot and does not need backtracking. That is faster and clearer than either.

Escaping

Characters with a special meaning need a backslash to match literally: . * + ? ( ) [ ] { } ^ $ | \. A dot in a domain name is \., not . — and example.com as a pattern will happily match exampleXcom.

Inside a character class most of them lose their meaning, so [.] is just a dot. Three still matter there: ^ at the start negates, - between characters makes a range, and ] ends the class.

Capture groups

Parentheses group, and they also capture what matched for later use. This is what makes find-and-replace powerful: match (\w+)@(\w+)\.com and you can refer back to the two halves as $1 and $2 in the replacement.

If you only want grouping without capturing, use (?:…). And in anything longer than one line, name them — (?<year>\d{4}) reads far better than counting brackets to work out which group is which.

Flags change everything

  • g — find every match, not just the first.
  • i — ignore case.
  • m — make ^ and $ match at each line rather than only the whole string.
  • s — let . match newlines, which it does not by default. This one catches people out on multi-line input constantly.

When not to use a regex

This matters more than any pattern here.

  • HTML and XML. They nest arbitrarily and regexes cannot count nesting. Use a parser. Every “it works for my case” version breaks on an attribute containing a bracket.
  • Validating email addresses. The real specification permits things no one expects, and the patterns people paste reject valid addresses. Check for an @ with something either side, then send a confirmation email — that is the only test that proves anything anyway.
  • Anything a plain string function does. includes, startsWith and split are clearer and faster.
  • Structured formats with a parser. JSON, CSV, dates — use the parser. It handles the quoting and escaping you will get wrong.

One performance trap worth knowing

Nested quantifiers over overlapping patterns — (a+)+$ is the classic — can take exponential time on input that nearly matches. On a server handling user input that is a denial of service, and it has taken down real sites. If a pattern has a quantifier inside a group that is itself quantified, look at it again.

Build them piece by piece

Nobody writes a working regex in one go. Start with the simplest thing that matches one example, then widen it until it covers the rest, testing against real input at every step — including the input that should not match, which is the half people skip.

The regex tester highlights matches and capture groups live as you type. For the jobs that do not need a regex at all, there is find and replace, extract emails and URLs from text, and sort lines.

Tools for this

Everything below runs in your browser. Nothing is uploaded.