Regular Expressions cheat sheet

A scannable Regular Expressions reference: 23 short snippets across 13 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Regular expressions basicsParentheses (…) capture a part so you can extract or backreference it. The pipe | means 'or'. Escape special characterslesson
Character classes, quantifiers and anchorsA character class matches exactly one character from a set. Everything inside the square brackets is a set, never alesson
Groups, captures and backreferencesParentheses do two jobs at once: they group for a quantifier or an alternation, and they record the text they matchedlesson
Lookahead and lookbehindA lookahead is a zero-width assertion about what follows the current position. Because it consumes nothing, thelesson
Flags and matching modesHow global, case-insensitive, multiline, dotAll, Unicode and sticky modes change a match, and where to put themlesson
Regex in JavaScriptA regex object with the g or y flag carries a lastIndex that every exec and test call updates. A module-level regexlesson
Regex in PythonThe re module: search, match and fullmatch, findall against finditer, group access, compiled patterns, and raw stringslesson
Dialects and engines: PCRE, POSIX, RE2 and .NETLeftmost-longest is the POSIX rule: among matches starting at the earliest position, take the longest. Perl-stylelesson
Practical patterns: validation, logs and URLsAlternation is ordered, so (?:25[0-5]|2[0-4]\d|1?\d?\d) must list the strict branches first. Putting 1?\d?\d at thelesson
Text surgery: find and replace across a codebaseThe preview is the whole method. Regex replacement fails quietly: a pattern that over-matches produces valid-lookinglesson
Greedy, lazy and catastrophic backtrackingPerl-style engines walk the pattern and the input together, and when a later part fails they return to a choice pointlesson
Unicode and character propertiesA JavaScript string is a sequence of UTF-16 code units. A character outside the basic plane, such as an emoji or a rarelesson
When not to use regexA regular expression recognises a regular language. Anything with matched nesting, escapes that change meaning, or alesson

Quick snippets

Regular expressions basics

Groups and alternation

const re = /(\d{4})-(\d{2})-(\d{2})/; // YYYY-MM-DD
const m = re.exec('2026-09-17');
console.log(m[1], m[2], m[3]); // 2026 09 17

Groups and alternation

import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-09-17')
print(m.groups())  # ('2026', '09', '17')

Full lesson: Regular expressions basics →

Character classes, quantifiers and anchors

Classes: ranges, negation and shorthands

const code = /^[A-Z]{2}-\d{4}$/;       // two letters, a dash, four digits
code.test('AB-1042');                    // true
code.test('ab-1042');                    // false, lowercase
code.test('AB-10422');                   // false, the $ anchor rejects the extra digit

const token = /[^\s,;]+/g;               // runs of non-delimiter characters
'a, b ;; c'.match(token);                // [ 'a', 'b', 'c' ]

/^[\w.+-]+@[\w-]+\.[\w.]+$/.test('[email protected]');   // true

Quantifiers: greedy, lazy and counted

const html = '<b>one</b> and <b>two</b>';

html.match(/<b>.*<\/b>/)[0];     // '<b>one</b> and <b>two</b>'   greedy
html.match(/<b>.*?<\/b>/)[0];    // '<b>one</b>'                  lazy
html.match(/<b>[^<]*<\/b>/)[0];  // '<b>one</b>'                  negated class

/<\d{2,4}>/.test('<123>');       // true, two to four digits
/<\d{2,4}>/.test('<1>');         // false
/colou?r/.test('color');          // true, the u is optional
/^ab*$/.test('a');                // true, a plus zero b

Anchors and word boundaries

/^\d{5}$/.test('12345');       // true, the whole string
/^\d{5}$/.test('12345 ');      // false, a trailing space
/\d{5}/.test('12345 ');        // true, no anchors, so a fragment is enough

/^\d+$/m.test('x\n12345');     // true: with m, ^ and $ also match at line breaks

/\bcat\b/.test('a cat sat');   // true
/\bcat\b/.test('concatenate'); // false, no boundary inside the word
/\bcat\b/.test('cat-like');    // true, a hyphen is a non-word character

Full lesson: Character classes, quantifiers and anchors →

Groups, captures and backreferences

Capturing versus non-capturing

// grouping only, nothing captured
/^(?:https?|ftp):\/\//.exec('https://example.com')[0];   // 'https://'

// captures are read from the match array
const m = /(\d{4})-(\d{2})-(\d{2})/.exec('2026-09-18');
m[0];        // '2026-09-18'  the whole match
m[1];        // '2026'
m[3];        // '18'
m.index;     // 0, offset of the match in the subject
m.length;    // 4, one entry per group plus the whole match

Named and nested groups

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { year, month, day } = re.exec('2026-09-18').groups;
// year '2026', month '09', day '18'

// nesting is numbered by the position of each opening bracket
const nested = /((\d{4})-(\d{2}))-(\d{2})/.exec('2026-09-18');
nested[1];   // '2026-09'
nested[2];   // '2026'
nested[3];   // '09'
nested[4];   // '18'

Backreferences

/(\w)\1/.exec('look')[0];            // 'oo'
/(['"]).*?\1/.exec('say "hi"')[0];      // '"hi"'
/(?<q>['"]).*?\k<q>/.exec('he said "stop"')[0];   // '"stop"'

// the trap: inside a repeated group, \1 remembers only the last iteration
/(\w)+\1/.exec('abcc')[0];             // 'abcc', because group 1 ended on 'c'

// what you probably wanted instead
/^(\w)\1/.test('aabb');                // true: a doubled letter at the start

Full lesson: Groups, captures and backreferences →

Lookahead and lookbehind

Lookbehind and its limits

import re

re.search(r'(?<=\$)\d+', 'price $42').group()          # '42'
re.search(r'(?<!\$)\d+', 'price $42, qty 3').group()   # '3'

# fixed width only: every alternative must have the same length
re.search(r'(?<=cat|dog)nap', 'a catnap')               # fine, both are three characters
re.search(r'(?<=cat|cats )nap', 'the cats nap')         # raises re.error: widths differ

Full lesson: Lookahead and lookbehind →

Flags and matching modes

The flags that matter

const re = /^ab.$/;
re.test('ab\nc');                 // false, . does not cross the newline
/^ab.$/s.test('ab\nc');           // true with dotAll
/^ab$/m.test('ab\nzz');           // true: with m, $ matches before the newline
/^zz$/m.test('ab\nzz');           // true: with m, ^ matches after the newline

/HELLO/i.test('hello');            // true
'aaa'.match(/a/g).length;          // 3, without g you would get one

const m = /(\d)(?<u>\d)/d.exec('42');
m.indices[0];                      // [ 0, 2 ]
m.indices.groups.u;                // [ 1, 2 ]

How modes move the anchors

import re

text = 'first\nsecond\nthird'

re.findall(r'^\w+', text)              # ['first']            ^ is string start only
re.findall(r'^\w+', text, re.M)        # ['first', 'second', 'third']
re.findall(r'\w+$', text)              # ['third']
re.findall(r'\w+$', text, re.M)        # ['first', 'second', 'third']

# \A and \z ignore the multiline mode entirely
re.findall(r'\A\w+', text, re.M)      # ['first']

Inline and scoped modifiers

// JavaScript has no inline flags; the flag belongs to the whole pattern
const ci = /hello/i;

// build the pattern dynamically when the mode has to vary at runtime
function literal(text, flags) {
  return new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
}
literal('a.b', 'i').test('A.B');   // true: the dot is escaped, so only a real dot matches
literal('a.b', 'i').test('AxB');   // false, the pattern no longer behaves as a wildcard

Full lesson: Flags and matching modes →

Regex in JavaScript

Literal versus constructor

const literal = /a+b/i;                       // compiled once, flags fixed
const dynamic = new RegExp('a+b', 'i');       // built at runtime

// escaping the hard way: a user-supplied search term is not a pattern
function escapeRegExp(text) {
  return text.replace(/[.*+?^()|[\]\\{}$#!]/g, '\\$&');
}

const userInput = 'price (net)';
new RegExp(escapeRegExp(userInput)).test('the price (net) today');   // true
new RegExp(userInput).test('the price (net) today');                 // throws: unbalanced group

Full lesson: Regex in JavaScript →

Regex in Python

Three ways to start a match

import re

re.search(r'\d+', 'id 42').group()          # '42'
re.match(r'\d+', 'id 42')                   # None: match anchors at position 0
re.fullmatch(r'\d+', '42')                  # matches; '42 ' would not
re.search(r'\d+', 'id 42').span()           # (3, 5)
re.search(r'\d+', 'id 42').start()          # 3

# an optional leading sign, then digits, anchored both ends
re.fullmatch(r'[+-]?\d+(\.\d+)?', '-3.14') is not None    # True

Groups, findall and groupdict

import re

m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', 'on 2026-09-18')
m.group(0)          # '2026-09-18'
m.group('year')     # '2026'
m.groupdict()       # {'year': '2026', 'month': '09', 'day': '18'}
m.groups()          # ('2026', '09', '18')

# findall changes shape depending on the number of groups
re.findall(r'\d+', 'a1 b22')                  # ['1', '22']
re.findall(r'(\w)(\d+)', 'a1 b22')           # [('a', '1'), ('b', '22')]
re.findall(r'(\w)(\d+)', 'a1 b22')[0][0]     # 'a'

Full lesson: Regex in Python →

Dialects and engines: PCRE, POSIX, RE2 and .NET

Who implements what

# POSIX ERE: no shorthand classes, and + must be escaped in BRE
echo "abc123" | sed -E 's/[0-9]+/N/'        # abcN
echo "abc123" | sed 's/[0-9]\+/N/'          # abcN, BRE needs the backslash

# POSIX classes are the portable spelling inside brackets
grep -E '^[[:alpha:]]+[[:space:]]+[[:digit:]]+$' data.txt

# word boundary is not portable: \b is a GNU extension
grep -Ew 'cat' data.txt                      # -w is the portable intent

RE2 and the linear-time promise

rejected by RE2            accepted by RE2
  (a+)+b                     a+b
  (\w+)\1                    \w+\s+\w+
  (?<=\$)\d+                  \$\d+          (match the symbol, then skip it)
  (?i)inline                 (?i)inline is fine in RE2
  possessive a++             possessive is accepted, it has no cost to remove

Full lesson: Dialects and engines: PCRE, POSIX, RE2 and .NET →

Practical patterns: validation, logs and URLs

Dates, IP addresses and CSV fields

// ISO date, shape only; validate the day count in code
const ISO = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
ISO.exec('2026-09-18').slice(1);       // [ '2026', '09', '18' ]
ISO.test('2026-13-01');                // false, month 13

// IPv4: the octet group must forbid values above 255
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
IPV4.test('192.168.0.1');              // true
IPV4.test('256.1.1.1');                // false

// one CSV field, quoted form first so the alternation order matters
const FIELD = /(?:^|,)(?:"([^"]*)"|([^",]*))/g;

Full lesson: Practical patterns: validation, logs and URLs →

Text surgery: find and replace across a codebase

Reviewing the edit

git add -A                     # stage everything, then inspect
git diff --cached --stat       # how many files and how many lines
git diff --cached              # read it, do not skim it

git diff --cached -G'fetch_user'      # only hunks touching the new name
git diff --cached | grep -c '^+'      # count added lines as a sanity check

git checkout -- .              # abandon the whole edit if the diff is wrong

Full lesson: Text surgery: find and replace across a codebase →

Greedy, lazy and catastrophic backtracking

How the engine actually searches

pattern: a*a  input: aaaab

attempt 1: a* takes aaaa, then needs 'a', fails
attempt 2: a* gives one back, takes aaa, then needs 'a', fails
attempt 3: ... one backtrack per character, linear

pattern: (a+)+b  input: aaaaaaaa...c

the outer + can split the run into groups in 2^n ways,
and every split is tried before the engine gives up

Patterns that explode

// measuring instead of guessing: this returns in milliseconds for short input
function timePattern(re, input) {
  const t0 = performance.now();
  re.test(input);
  return performance.now() - t0;
}

const re = /^(a+)+$/;
timePattern(re, 'a'.repeat(20));   // fast
timePattern(re, 'a'.repeat(28));   // noticeably slower
timePattern(re, 'a'.repeat(32));   // seconds, then minutes

Full lesson: Greedy, lazy and catastrophic backtracking →

Unicode and character properties

Normalisation and case folding

// case-insensitive comparison of text that may be decomposed
const strip = s => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();

strip('Caf\u00e9');       // 'cafe'
strip('Cafe\u0301');     // 'cafe'

// the i flag is not the same as case folding: some characters do not fold simply
/\p{Ll}/u.test('a');      // true
/^\p{Lu}$/iu.test('a');   // true only because i is set

// match word characters in any script, then trim the result in code
'\u4f60\u597d world'.match(/\p{L}+/gu);   // [ '\u4f60\u597d', 'world' ]

Full lesson: Unicode and character properties →

When not to use regex

Grammars, state machines and balanced input

# two-step: regex for the shape, code for the structure
import re, json

def extract_objects(text):
    """Regex finds candidate blocks; a real parser decides which are valid."""
    for m in re.finditer(r'\{[^{}]*\}', text):
        try:
            yield json.loads(m.group(0))
        except json.JSONDecodeError:
            continue          # a false positive from the pattern is simply skipped

list(extract_objects('noise {"a":1} more {"b": [2]}'))

Full lesson: When not to use regex →

FAQ

Is this Regular Expressions cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 13 lessons of the Regular Expressions course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Regular Expressions course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Algorithms Data Structures Computer Networks Operating Systems Character Encodings Hashing & Checksums

Last refreshed 2026-09-27.