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.
// anchors & boundaries
^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
.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[.] 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
*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? after any of the abovemakes it lazy — matches as few repetitions as possible instead of as manyGiven 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.
<.*> (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
(...)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:
(?<name>...) — backreference \k<name>(?<name>...) — same as .NET; backreference \k<name>(?<name>...) or (?P<name>...) — both accepted(?P<name>...) — backreference (?P=name), the P is required(?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
(?=...)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 thisLookaround 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:
i / .NET IgnoreCasecase-insensitive matchingm / .NET Multiline^ and $ match at every line break, not just string start/ends (dotAll) / .NET Singlelinemakes . match newlines too — .NET's name is famously the opposite of what it sounds like it doesgglobal — find all matches, not just the first (implicit in .NET's Matches())ysticky — match only starting exactly at lastIndex, no scanning forwardu / 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 oncedhasIndices — also return the start/end index of each match, not just the textSingleline 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)
^\d+$^(\d{1,3}\.){3}\d{1,3}$ — matches shape only, not that each octet is ≤255^[a-z0-9]+(-[a-z0-9]+)*$^\s+|\s+$ → replace with empty string\s+^#([0-9a-fA-F]{3}){1,2}$^[^\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+ 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.(a+)+, (a*)* — a quantified group inside another quantifier(a|a)*, (a|ab)* — branches that can match the same text multiple waysThe 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
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.+ 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.
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.