What do you actually need from regular expressions to get through a workday? Most of us revisit the same handful of tokens, re-learn lookaround every six months, then forget it again. This regex cheatsheet is the slice that covers the patterns I reach for in code reviews, log parsing, input validation, and search-and-replace. Skip the 400-page spec. Memorize the bits below and you will handle roughly 80% of real jobs.

Anchors: ^ $ \b

Anchors don’t match characters. They match positions, which is why they’re cheap and easy to misuse.

^TODO          # a line that starts with TODO
\.$            # a line ending with a literal period
\berror\b      # the word "error", not "errors" or "terror"

^ and $ are line start and end in multiline mode (/m flag). Without it, they’re string start and end. \b sits between a word character \w and a non-word character, which lets you match whole words without capturing surrounding text. The most common mistake is assuming ^ means “not” — that only lives inside character classes, covered next.

Character classes: \d \w \s and their negations

The shorthand classes collapse the most frequent character groups:

\d    # digit         [0-9]
\w    # word char     [A-Za-z0-9_]
\s    # whitespace    [ \t\n\r\f\v]
\D    # NOT a digit
\W    # NOT a word char
\S    # NOT whitespace

A useful digression: \w includes the underscore, which surprises people the first time they try to split identifiers. For custom sets, build your own bracket class.

[aeiou]         # any vowel
[A-Z]           # uppercase letter
[0-9a-fA-F]     # hex digit
[^0-9]          # anything except a digit (^ inside [] means negation)
[-.]            # literal dash or period (dash first or last, else it's a range)

The leading ^ inside brackets is the only place that character means “not.” Outside, it’s an anchor. Inside, a dash between two characters is a range; placed at the start or end, it’s a literal.

Quantifiers: * + ? {n,m} and greediness

Quantifiers say how many times the previous token repeats.

a*       # zero or more
a+       # one or more
a?       # zero or one
a{3}     # exactly 3
a{2,4}   # between 2 and 4
a{2,}    # 2 or more

By default, quantifiers are greedy — they swallow as much as possible and backtrack only if the rest of the pattern fails. Add ? to make them lazy.

<.*>      # greedy: matches from first < to LAST > on the line
<.*?>     # lazy:   matches first < to first >  (one HTML tag)

The lazy variant is how you stop a tag matcher from eating a whole document. For performance on large inputs, prefer negated character classes (<[^>]*>) over lazy quantifiers when you can — they avoid backtracking.

Groups, alternation, capture vs non-capture

Parentheses group. They also capture by default, which has a cost and an effect on backreferences.

cat|dog          # alternation: cat OR dog
(gra|gra)ss      # redundant, but shows grouping
(\d{4})-(\d{2})  # captures year and month separately
(?:\d{4})        # non-capturing group — groups without saving

Use (?:...) when you need grouping for alternation or quantifiers but don’t care about the value. It’s faster and keeps your match groups clean. Capture groups become $1, $2 in substitutions and \1, \2 inside the same pattern — handy for matching a repeated word:

\b(\w+)\s+\1\b    # finds doubled words like "the the"

Lookaround: (?=...) (?!...) (?<=...) (?<!...)

Lookaround asserts that something does or doesn’t match at a position, without consuming characters. This is the part everyone forgets between uses.

foo(?=bar)      # "foo" only when followed by "bar"
foo(?!bar)      # "foo" only when NOT followed by "bar"
(?<=\$)\d+      # digits preceded by a literal $
(?<!un)\w+      # a word NOT preceded by "un"

Lookbehind has a quirk: the pattern inside must be fixed-length in many engines (JavaScript gained variable-length lookbehind in ES2018 and all modern browsers now support it). Lookahead has no such limit. The power move is matching without capturing — (?<=\$)\d+ grabs the number after a currency sign while leaving the sign out of the result.

Six recipes you can paste today

Each pattern below is testable in the Regex Tester. Drop it in, switch on the flags noted in comments, and check matches before shipping.

# 1. Email-ish (good enough for form validation, not RFC 5322)
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$

# 2. HTTP(S) URL
https?://[A-Za-z0-9.-]+(?:/[^\s]*)?

# 3. US-style phone: (555) 123-4567 or 555-123-4567
\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}

# 4. ISO 8601 date: 2026-06-04
^\d{4}-\d{2}-\d{2}$

# 5. Trim leading/trailing whitespace (use with global replace, replace with "")
^\s+|\s+$

# 6. Hex color: #A3C or #A3C113
^#?(?:[0-9a-fA-F]{3}){1,2}$

None of these are bulletproof. The email pattern allows + aliases (the + is in the character class) but rejects quoted local parts, IP-literal hosts, and internationalized characters. The URL pattern does not enforce TLDs. Treat them as triage filters, not parsers. When a recipe stops fitting, the Text Diff Checker is handy for comparing what your old pattern matched against the new one before you swap them.

FAQ

Should I learn regex or just use an AI prompt?

Both, in that order. AI is good at drafting a pattern from a description, but you still need to read what it produced, because AI routinely ships patterns that match too much or too little. Knowing the anchors, quantifiers, and lookaround in this cheatsheet is what lets you spot a wrong pattern before it lands in production.

Why does my regex work on one site but fail in my code?

Engines differ. JavaScript did not support lookbehind until ES2018, POSIX grep uses BRE by default (no + or ? shorthand), and PCRE has features Python’s re module lacks. Check which engine your runtime uses and test the pattern there. The flags matter too — ^ and $ behave differently without /m.

What is the fastest way to debug a regex that doesn’t match?

Make it permissive, then tighten. Start with the broadest token (drop quantifiers, replace specific chars with .) and add constraints back one at a time until matches disappear. That tells you exactly which token broke. An online tester that highlights each group as you type saves more time than reading docs.

The patterns worth keeping

Keep these on a sticky note: ^ and $ for boundaries, \b for whole words, \d \w \s and their uppercase negations, + and *? for repetition, (?:...) for non-capturing groups, and the four lookaround operators for assertions without consumption. Combine those with a couple of bracket classes and you have the working vocabulary for almost every regex task that shows up in real code.