Regex Cheat Sheet

The syntax always looks the same three seconds after you stop needing it. This is the reference: every token, why .* is usually the wrong answer, how the same pattern is spelled differently in .NET vs JavaScript vs Python, and the one mistake that has taken down Stack Overflow and Cloudflare.

reference guide .NET JavaScript PCRE / Python

// anchors & boundaries

Anchors don't match a character — they match a position. That's why ^$ alone matches an empty line, and why forgetting one is the single most common reason a pattern matches more than you intended.

syntax
^start of string (or start of line, with the multiline flag)
$end of string (or end of line, with the multiline flag)
\ba word boundary — between a \w and a non-\w character, matches nothing itself
\Bnot a word boundary — the inverse of \b
\Aabsolute start of string, unaffected by the multiline flag — .NET, Python, PCRE (not JavaScript)
\zabsolute end of string, unaffected by the multiline flag — .NET, Python, PCRE (not JavaScript)

// character classes

syntax
.any character except a newline (unless the dotAll/Singleline flag is set)
\d / \Da digit / not a digit — [0-9] and its inverse
\w / \Wa word character / not one — [A-Za-z0-9_] and its inverse
\s / \Swhitespace / not whitespace — includes space, tab, newline
[abc]any one of a, b, or c
[^abc]any character except a, b, or c
[a-z]a range — any lowercase letter
\p{L}a Unicode property class — e.g. any letter in any script. Needs the u/v flag in JS
Inside a character class, most metacharacters lose their special meaning — [.] means a literal dot, not "any character." The exceptions that still need escaping inside [ ] are ], \, ^ (if it's the first character), and - (if it isn't first or last).

// quantifiers — and why greedy surprises people

syntax
*0 or more, greedy
+1 or more, greedy
?0 or 1, greedy (also marks a group as lazy — see below)
{n}exactly n times
{n,}n or more times
{n,m}between n and m times
add ? after any of the abovemakes it lazy — matches as few repetitions as possible instead of as many

Given the input <b>bold</b> and <i>italic</i>, the greedy pattern <.*> matches the entire string — from the first < to the very last > — because greedy means "grab as much as possible, then backtrack only if forced to." The lazy version <.*?> matches just <b>, stopping at the first > it finds. Reaching for .* against tag-like or delimiter-like text is the single most common source of an "it matched way too much" bug.

<b>bold</b> and <i>italic</i>greedylazy
Same input, two quantifiers: <.*> (greedy) consumes from the first < all the way to the very last >. <.*?> (lazy) stops at the first > it finds.

// groups & backreferences — syntax differs by engine

syntax
(...)capturing group — saved and numbered left-to-right by opening parenthesis
(?:...)non-capturing group — groups for precedence/quantifying without saving a match
|alternation — cat|dog matches either, scoped to the nearest group
\1, \2...backreference to a numbered group — "whatever group 1 matched, again"

Named groups exist in every mainstream engine, but the syntax genuinely differs — this trips people up when copying a pattern between a .NET backend and a JS frontend:

named group syntax by engine
.NET (C# / VB.NET)(?<name>...) — backreference \k<name>
JavaScript (ES2018+)(?<name>...) — same as .NET; backreference \k<name>
PCRE (PHP, older engines)(?<name>...) or (?P<name>...) — both accepted
Python(?P<name>...) — backreference (?P=name), the P is required
Python pioneered named groups with (?P<name>...), but every other major engine, including PCRE, later converged on the angle-bracket form without the P. If a pattern with (?P<...>) fails in .NET or JavaScript, that's why.

// lookaround

syntax
(?=...)positive lookahead — must be followed by this, but it isn't part of the match
(?!...)negative lookahead — must not be followed by this
(?<=...)positive lookbehind — must be preceded by this
(?<!...)negative lookbehind — must not be preceded by this
Lookbehind is newer than lookahead in JavaScript — it only shipped in ES2018 (Chrome/V8, then other engines followed). It's been in .NET and PCRE for far longer. If a lookbehind pattern works everywhere except an old Safari build, that's the reason — check current engine support before relying on it for a public-facing site.

Lookaround is how you match "a digit that's followed by px" without capturing the px itself: \d+(?=px) against 16px matches 16 only. It's a zero-width assertion — it checks, then discards, contributing nothing to the matched text.

// flags — JS engine names vs .NET RegexOptions

The regex-tester tool on this site runs on JavaScript's engine but labels its checkboxes to match .NET's RegexOptions enum, since that's the more common naming most people learn. Here's how the two map:

flags
JS i / .NET IgnoreCasecase-insensitive matching
JS m / .NET Multiline^ and $ match at every line break, not just string start/end
JS s (dotAll) / .NET Singlelinemakes . match newlines too — .NET's name is famously the opposite of what it sounds like it does
JS gglobal — find all matches, not just the first (implicit in .NET's Matches())
JS ysticky — match only starting exactly at lastIndex, no scanning forward
JS u / vUnicode mode — v (ES2024) is a stricter superset of u that also enables set operations in character classes; a pattern can't use both at once
JS dhasIndices — also return the start/end index of each match, not just the text
.NET's Singleline and JavaScript's m/multiline sound related but aren't the same axis — .NET Singleline = JS s (dotAll: affects .), while Multiline in both languages affects ^/$. Reading ".NET Singleline" as "the opposite of multiline" is the natural but wrong assumption.

// patterns worth memorizing (and one that's a trap)

common patterns
Digits only^\d+$
IPv4 address^(\d{1,3}\.){3}\d{1,3}$ — matches shape only, not that each octet is ≤255
Slug (URL-safe)^[a-z0-9]+(-[a-z0-9]+)*$
Trim outer whitespace^\s+|\s+$ → replace with empty string
Split on any whitespace run\s+
Hex color code^#([0-9a-fA-F]{3}){1,2}$
There is no correct "email regex." The actual email spec (RFC 5322) is absurd — quoted local parts, comments, IP-literal domains — and the patterns copy-pasted from Stack Overflow either reject valid addresses or accept garbage. If you need to know an email is real, send a verification email; use a regex like ^[^\s@]+@[^\s@]+\.[^\s@]+$ only as a cheap first-pass sanity check, never as the source of truth.

// the mistake that takes down production: catastrophic backtracking

Backtracking engines (JS, .NET, PCRE, Python — everything except a few regex-fixed-time engines like RE2) try a match, and if it fails, rewind and try a different split. Certain patterns create ambiguous ways to split the same input — nested or overlapping quantifiers like (a+)+ or (a|a)* — and the number of ways to backtrack grows exponentially with input length. A 20-character string can take milliseconds; a 40-character string of the wrong shape can take longer than the universe has left. This is a real, named vulnerability class: ReDoS (Regular expression Denial of Service).

a+ — one way to matchalways the same path(a+)+ — many ways to split itdoubles with each extra character
A safe quantifier like a+ has exactly one way to consume a run of characters. A nested quantifier like (a+)+ can split the same run many different ways — the count roughly doubles with each extra character, which is what turns backtracking exponential instead of linear.
This isn't theoretical: a catastrophic-backtracking regex caused a 34-minute Stack Overflow outage in July 2016, and a faulty WAF rule caused a 27-minute global Cloudflare outage in July 2019. Both were shipped patterns that looked reasonable and passed code review.
what to watch for
Nested quantifiers(a+)+, (a*)* — a quantified group inside another quantifier
Overlapping alternation(a|a)*, (a|ab)* — branches that can match the same text multiple ways
Any user-controlled patterna regex built from user input, or run against untrusted input with no length/time limit, is the actual attack surface

The fix is usually straightforward once you see it: make the repeated unit unambiguous (a+ instead of (a+)+), prefer a possessive or atomic group where the engine supports one, or cap input length before running the pattern at all.

// two more that only bite in production

The g flag makes a JavaScript RegExp object stateful. With g set, calling .test() or .exec() repeatedly on the same compiled regex object advances its internal lastIndex each time — reusing one global-flagged regex across multiple independent checks silently skips matches after the first call. Create a fresh regex (or reset lastIndex = 0) per independent check.
Forgetting to escape a literal special character — a period in a version number, a + in an email's local part, a $ in a price — makes the pattern match more than intended rather than throwing an error, since almost every metacharacter is also valid-looking as "any character" or "one or more." A dot meant to match a literal . will happily match any character instead, and the bug only surfaces on input that exploits the difference.

// try it yourself

Paste a pattern into the Regex Tester for live match and capture-group highlighting, a plain-English breakdown of each token, and generated C# and JavaScript code — entirely in your browser, nothing sent anywhere.