Polyatic

Regex Quick Reference

A regular expression is a small pattern language for matching, extracting and reshaping text. This is a reference you can learn from and keep open in a tab: every token below comes with a concrete example, greedy versus lazy is shown side by side, all four lookarounds are worked through, and the table of "real" patterns at the end is honest about what each one quietly fails to catch. The syntax here is the JavaScript / PCRE common core — flavours differ at the edges, noted where it matters.

Character classes — what a single position can be

A character class matches exactly one character out of a set. The shorthands are the ones you type all day, and each has an uppercase negation that means "anything but":

TokenMatches one character that is…
\da digit, 0-9. Negation \D = any non-digit
\wa "word" char: A-Z a-z 0-9 _. Negation \W. Note it includes the underscore and, without the u flag, excludes accented letters like é
\swhitespace: space, tab, newline, carriage return and a few more. Negation \S
[a-z]a custom range — any lowercase ASCII letter. Combine them: [A-Za-z0-9_] is the same set as \w
[^…]a NEGATED custom class — the ^ as the first char flips it: [^0-9] is any non-digit
.any character except a line break (see the limit below)

Inside […] most metacharacters lose their power and stand for themselves — [.+*] is a literal dot, plus or star, no escaping needed. A hyphen means a range unless it is first or last: [-a-z] matches a hyphen or a lowercase letter.

The honest limit of the dot. . is not "any character". It skips line breaks by default (turn on the s / dotAll flag to include them), and it works on UTF-16 code units, so without the u flag a single emoji can register as two characters. When you mean a literal period, escape it: an unescaped pattern 3.14 also matches 3x14, because the dot there is a wildcard.

Anchors — positions, not characters

Anchors match a place in the string and consume no characters. They are how you say "at the start", "at the end" or "at a word edge".

  • ^ — start of the string (or start of any line when the m / multiline flag is on).
  • $ — end of the string (or end of any line under m). So ^\d+$ means "the entire string is nothing but digits" — the anchors are what forbid stray characters on either side.
  • \b — a word boundary: the seam between a \w and a \W (or the string edge). \bcat\b matches cat in the cat sat but not the cat inside concatenate. Its negation \B matches a non-boundary.

A common bug: leaving off the anchors. \d{4} matches "four digits somewhere", so it happily accepts abc1234xyz; ^\d{4}$ demands the whole string be exactly four digits.

Quantifiers — how many, and greedy vs lazy

A quantifier says how many times the preceding item may repeat:

TokenRepeats the previous item…
*zero or more times
+one or more times
?zero or one time (makes it optional)
{3}exactly 3 times
{2,5}between 2 and 5 times, inclusive
{2,}2 or more times (no upper bound)

By default every quantifier is greedy: it grabs as many characters as it can, then hands them back one at a time only if the rest of the pattern can't otherwise match. Append a ? to make it lazy (also called non-greedy): it takes as few as possible and expands only when forced. This is the single distinction that causes the most "why did my regex eat the whole line" confusion, so here it is worked through against one input:

Input: <a><b> Greedy <.+> → matches <a><b> (the WHOLE string) .+ first swallows a><b then backtracks to the LAST > Lazy <.+?> → matches <a> (just the first tag) .+? takes one char (a) then stops at the FIRST > Class <[^>]+> → matches <a> (and it is faster) [^>]+ simply can't cross a >, so no backtracking at all

The takeaway: greedy is the right default for most matching, but when your closing delimiter can appear more than once on a line, you want the lazy form .+? — or better, a negated class like [^>]+, which expresses the intent directly and avoids backtracking entirely. Beware ? doing double duty: in a? it is the "optional" quantifier, but in a+? it is the laziness modifier on the +.

Grouping, alternation and backreferences

Parentheses bundle several tokens into one unit. There are two kinds, and the difference is whether the engine remembers what they matched:

  • Capturing (…) — groups the tokens and stores the matched text in a numbered slot you can read back. (ab)+ matches ababab and group 1 holds the last ab. In a replacement you reference captures as $1, $2; named captures use (?<year>\d{4}) read as $<year>.
  • Non-capturing (?:…) — groups only for structure, storing nothing. Use it to apply a quantifier or alternation to a chunk without spending a group number: (?:ab)+ repeats ab but captures nothing, which keeps later group numbers stable and is fractionally cheaper.
  • Alternation | — "either side". It has the lowest precedence, so ^cat|dog$ means ^cat OR dog$, not "cat or dog" bounded. Parenthesise it: ^(cat|dog)$. Prefer non-capturing (?:cat|dog) when you don't need the capture.
  • Backreference \1 — matches the same text a prior capture matched, not the same pattern. (["']).*?\1 matches a quoted string and guarantees the closing quote is the same character as the opening one; \b(\w+)\s+\1\b finds a doubled word like the the. Named backreferences are \k<name>.

Lookarounds — assert without consuming

A lookaround checks that something does (or does not) appear next to the current position, but matches zero characters — the checked text stays available for the rest of the pattern. There are four, split by direction (ahead/behind) and polarity (positive/negative):

TokenAsserts that…
(?=…)positive lookahead — what follows does match
(?!…)negative lookahead — what follows does not match
(?<=…)positive lookbehind — what precedes does match
(?<!…)negative lookbehind — what precedes does not match

The point of a lookaround is to add a condition without pulling the checked text into the match. A worked example — extract the number from a price while requiring a leading $, without capturing the $ itself:

Input: Total: $42 and 7 apples Pattern: (?<=\$)\d+ (?<=\$) asserts a "$" sits immediately to the LEFT \d+ then matches the digits — and only those Match → 42 (the 7 has no $ before it, so it is skipped) The "$" is NOT part of the match, so no group juggling needed.

Negative lookahead is the classic way to say "…but not if followed by X". \bcolou?r\b(?!s) matches color and colour but skips colors. A well-known trick, ^(?=.*\d)(?=.*[a-z]).{8,}$, uses two stacked lookaheads as independent AND conditions — "contains a digit" and "contains a lowercase letter" — each checked from the start without consuming anything, then .{8,} does the actual matching. Honest limit: JavaScript gained lookbehind only in ES2018, and older engines plus some flavours restrict it to fixed-length patterns; if you need broad portability, verify lookbehind is supported where the regex will run.

Real patterns — and what each one misses

These are patterns people actually reach for. Every one is a shape check, and every one has holes; the note says exactly what it does not catch, because a validating regex that quietly rejects (or accepts) the wrong thing is worse than none.

PatternIntended forWhat it does NOT catch (honest note)
[^@\s]+@[^@\s]+\.[^@\s]+email-ishValidates shape, not deliverability. It rejects some valid addresses (quoted local parts, no-dot intranet domains) and accepts plenty that no server will deliver to. The RFC 5322-correct regex is impractically long — use a loose check plus a confirmation email.
\d{4}-\d{2}-\d{2}ISO date (YYYY-MM-DD)Checks digit shape only. It happily accepts 2026-13-40 and 0000-00-00 — it does not know months stop at 12, days at 28–31, or that leap years exist. Range-check the numbers after matching.
#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\bhex colourMatches 3- and 6-digit hex only. It misses 4- and 8-digit forms with alpha (#rgba, #rrggbbaa), and of course rgb(), hsl() and named colours. Add {4}|{8} alternatives if you support alpha.
\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}US-ish phoneA loose North-American 10-digit shape. It ignores the +1 country code, extensions, and international formats entirely, and will match junk like 000.000.0000. For anything global, normalise digits and use a real library (e.g. libphonenumber) instead.

The pattern that ties into the greedy/lazy section: to strip HTML tags people write <.+?>, but <[^>]+> is safer and faster, and neither truly "parses HTML" — comments, CDATA and <script> bodies will fool both. Regex is superb for well-shaped tokens and a poor fit for recursive grammars like HTML or JSON; reach for a real parser there.

Test any of these live

Paste a pattern and your own text, watch every match highlight on each keystroke, inspect the capture groups, and preview a replacement — all in your browser, nothing sent anywhere.

Try the regex tester →

A note on flavours and safety

Almost everything above is portable across JavaScript, PCRE, Python's re and .NET. The edges differ: JavaScript has no atomic groups, possessive quantifiers or recursion; lookbehind arrived late and is fixed-length in some engines; \p{…} Unicode property classes need the u flag in JS. One safety point worth internalising — nested quantifiers over overlapping alternatives, such as (a+)+$ against a long run of as that fails at the end, cause catastrophic backtracking: the engine tries exponentially many splits and can freeze for seconds. Prefer specific negated classes ([^x]) over blanket .*, and avoid a quantifier wrapped around another quantifier where the inner pattern can match the same text many ways.

Open the regex tester → to try any pattern on this page against your own text, or browse all Polyatic tools.