A regex cheat sheet that explains itself

Updated 2026-08-28 ยท about 9 minute read

Regular expressions have a reputation for being write-only. They are not, but they do reward learning the pieces in the right order โ€” and there are perhaps twenty constructs that cover almost everything you will ever need.

The characters that do the work

PatternMatches
.Any character except a newline
\d / \DA digit / anything but a digit
\w / \WWord character (letter, digit, underscore) / not one
\s / \SWhitespace / not whitespace
[abc]Any one of a, b or c
[^abc]Any character except a, b or c
[a-z]Any character in the range
\.A literal full stop

The caret does two entirely different jobs depending on position: inside square brackets it negates, everywhere else it anchors to the start of a line. This is a common source of confusion and there is no way round it but to notice the brackets.

Escaping matters. . means "any character" until you write \., at which point it means a full stop. The characters needing escapes are . ^ $ * + ? ( ) [ ] { } | \ /. Inside a character class most of them lose their special meaning, so [.] is also just a full stop.

Everything below is easier to follow with a live matcher in front of you โ€” the regex tester highlights matches and capture groups as you type, which turns "why doesn't this work" into something you can see.

Quantifiers, and the greedy trap

PatternMeaning
*Zero or more
+One or more
?Zero or one โ€” optional
{3}Exactly three
{2,5}Between two and five
{2,}Two or more

Now the single most common regex bug. Quantifiers are greedy: they match as much as possible, then give characters back only if the rest of the pattern fails.

Run <.+> against <b>bold</b> and you might expect to match <b>. You get the entire string, because .+ swallowed everything up to the last >.

Add a question mark to make a quantifier lazy: <.+?> matches as little as possible and returns <b> as intended. This one character fixes an enormous proportion of "my regex matches too much" problems.

The better fix is often to be specific rather than lazy. <[^>]+> โ€” "angle bracket, then anything that is not a closing bracket, then a closing bracket" โ€” cannot overshoot by construction, and it is faster.

Groups, capture and alternation

Parentheses group and capture. (\d{4})-(\d{2})-(\d{2}) against 2026-08-28 captures the year, month and day as groups 1, 2 and 3, retrievable in a replacement as $1, $2, $3.

If you only want grouping without the capture, (?:...) is a non-capturing group. Worth using โ€” it keeps your group numbers meaningful when you add a group later.

Better still, name them: (?<year>\d{4}) lets you refer to year instead of counting parentheses. Anything you will still be reading in six months should use named groups.

The pipe means alternation: cat|dog matches either. Watch its precedence โ€” ^cat|dog$ means "starts with cat, or ends with dog", almost certainly not what was intended. Group it: ^(cat|dog)$.

Anchors and boundaries

  • ^ โ€” start of the string, or of a line with the multiline flag.
  • $ โ€” end of the string, or of a line.
  • \b โ€” a word boundary, the position between a word character and a non-word character.

Word boundaries are underrated. cat matches inside "concatenate"; \bcat\b does not. Any time a search matches the middle of a longer word, this is the fix.

Anchors match positions, not characters, so they consume nothing. That is why ^$ matches an empty line rather than nothing at all.

Lookahead and lookbehind

Lookaround asserts that something is or is not nearby, without consuming it.

  • (?=...) โ€” positive lookahead: followed by this.
  • (?!...) โ€” negative lookahead: not followed by this.
  • (?<=...) โ€” positive lookbehind: preceded by this.
  • (?<!...) โ€” negative lookbehind: not preceded by this.

So \d+(?= kg) matches the number in "70 kg" without capturing the unit, and (?<=ยฃ)\d+ matches the digits after a pound sign without taking the sign along.

Lookbehind is the newest of these and support was patchy for years. It works in modern JavaScript, Python, .NET, Java and PCRE, but check before relying on it in an unfamiliar runtime.

Two patterns you should not write

Do not write an email validator. The regex that fully implements RFC 5322 is several thousand characters long and still permits addresses no mail server accepts. Every shorter version rejects valid addresses โ€” apostrophes, plus signs, new top-level domains, international characters. Check that there is an @ with something either side, then send a confirmation email. The email is the only real validation.

Do not parse HTML with regex. HTML is not a regular language: it nests arbitrarily, and regex cannot count nesting. It will appear to work on your test input and fail on real pages with comments, CDATA sections, attributes containing angle brackets, or unclosed tags. Use a parser.

One more hazard worth knowing by name: catastrophic backtracking. Nested quantifiers such as (a+)+$ can take exponential time on input that nearly matches, which is a genuine denial-of-service vector โ€” a "ReDoS". If a regex runs on untrusted input, avoid nesting quantifiers, and prefer specific character classes to .*.

For plain text work that does not need regex at all, the text cleaner handles trimming, deduplicating and sorting lines, and the text compare tool shows what changed between two versions. Pikkit has the rest.

Try the tool

Frequently asked questions

What does \d mean in regex?

It matches any single digit, 0 to 9. Its opposite, \D, matches anything that is not a digit.

What is the difference between greedy and lazy matching?

Greedy quantifiers match as much as possible and then backtrack; lazy ones, written with a trailing question mark, match as little as possible. <.+> grabs a whole string where <.+?> stops at the first closing bracket.

How do I match a whole word only?

Wrap it in word boundaries: \bcat\b matches cat but not the cat inside concatenate.

Should I use regex to validate email addresses?

No. A fully correct pattern is thousands of characters and still imperfect. Check for an @ with text either side, then send a confirmation email.

Why is my regex slow or hanging?

Probably catastrophic backtracking from nested quantifiers such as (a+)+. On input that nearly matches, this can take exponential time. Use specific character classes instead of .* where you can.