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
| Topic | What it covers | |
|---|---|---|
| Regular expressions basics | Parentheses (…) capture a part so you can extract or backreference it. The pipe | means 'or'. Escape special characters | lesson |
| Character classes, quantifiers and anchors | A character class matches exactly one character from a set. Everything inside the square brackets is a set, never a | lesson |
| Groups, captures and backreferences | Parentheses do two jobs at once: they group for a quantifier or an alternation, and they record the text they matched | lesson |
| Lookahead and lookbehind | A lookahead is a zero-width assertion about what follows the current position. Because it consumes nothing, the | lesson |
| Flags and matching modes | How global, case-insensitive, multiline, dotAll, Unicode and sticky modes change a match, and where to put them | lesson |
| Regex in JavaScript | A regex object with the g or y flag carries a lastIndex that every exec and test call updates. A module-level regex | lesson |
| Regex in Python | The re module: search, match and fullmatch, findall against finditer, group access, compiled patterns, and raw strings | lesson |
| Dialects and engines: PCRE, POSIX, RE2 and .NET | Leftmost-longest is the POSIX rule: among matches starting at the earliest position, take the longest. Perl-style | lesson |
| Practical patterns: validation, logs and URLs | Alternation is ordered, so (?:25[0-5]|2[0-4]\d|1?\d?\d) must list the strict branches first. Putting 1?\d?\d at the | lesson |
| Text surgery: find and replace across a codebase | The preview is the whole method. Regex replacement fails quietly: a pattern that over-matches produces valid-looking | lesson |
| Greedy, lazy and catastrophic backtracking | Perl-style engines walk the pattern and the input together, and when a later part fails they return to a choice point | lesson |
| Unicode and character properties | A JavaScript string is a sequence of UTF-16 code units. A character outside the basic plane, such as an emoji or a rare | lesson |
| When not to use regex | A regular expression recognises a regular language. Anything with matched nesting, escapes that change meaning, or a | lesson |
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 characterFull 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 startFull 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 differFull 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 wildcardFull 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 groupFull 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 removeFull 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 wrongFull 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 minutesFull 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?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Algorithms Data Structures Computer Networks Operating Systems Character Encodings Hashing & Checksums
Last refreshed 2026-09-27.