Everything the adaptive question bank can teach and test in Regex, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
\q is a literal q in legacy mode but a syntax error under u/v
•
\n, \t, \r, \f, \v, \0 and the \cX control-character form
•
\xhh and \uhhhh everywhere; \u{10FFFF} only under u or v
•
new RegExp("\\d") needs two backslashes where /\d/ needs one
•
inside [] only ] \ ^ - need care, and position can substitute for escaping
•
RegExp.escape (ES2025) or re.escape; a raw user string is a pattern, not a search term
C. Character classes
•
[abc] consumes exactly one character drawn from the set
•
[cat] is c or a or t, and never the word "cat"
•
[a-z] is a codepoint range, so [z-a] is an error and [A-z] includes punctuation
•
[^a] matches one character, so it can never match at end of input
•
[^a] accepts \n even without the dotall flag, unlike .
•
a hyphen is literal when first, last, or escaped
•
[a^] matches a caret; only a leading ^ negates
•
[\d\s.-] unions shorthand escapes with literals
•
[^\d] is \D, and [^\d\D] can never match anything
•
[abc] is one comparison; (?:a|b|c) is three branches to backtrack through
D. Shorthand class escapes and the wildcard
•
. matches any character except line terminators until the dotall flag
•
\d is [0-9] in JS and PCRE by default, and any Unicode digit for a Python str pattern
•
\w is [A-Za-z0-9_], and the underscore is the part people forget
•
\s covers space, tab, CR, LF, FF, VT, and in JS also and Unicode spaces
•
\D, \W, \S match exactly what their lowercase forms do not
•
\w+ rejects café and Ω, which silently breaks a name validator
•
the same \w is ASCII in JS and Unicode-aware in Python's default str mode
•
Python's re.A and PCRE2's PCRE2_UCP toggle whether the shorthands see Unicode
E. Unicode mode, property escapes and set algebra
•
u makes a surrogate pair one unit and turns unknown escapes into errors
•
without u, . and [] operate on UTF-16 code units and cut an emoji in half
•
\u{1F600} is only legal under u or v; otherwise it reads as a braced quantifier
•
\p{L}, \p{N}, \p{Lu} select characters by Unicode general category
•
\p{Script=Greek} is narrower than \p{Script_Extensions=Greek}
•
\P{L} is the complement, and inside a v class it composes differently
•
v (ES2024) adds intersection &&, difference --, and nested classes
•
\p{RGI_Emoji} can match a multi-codepoint string, which no other escape does
•
a flag or family emoji is several codepoints; JS has no \X
•
under u, i folds K to the Kelvin sign and s to the long ſ
•
precomposed é and e + combining accent are different subjects to the engine
F. Quantifiers and repetition
•
zero-or-more, one-or-more, and optional
•
{n}, {n,} and {n,m}, and why {,m} is not an "up to m" form
•
ab* repeats only the b; (?:ab)* repeats the pair
•
(?:abc)? makes three characters optional as a unit
•
(?:a{2}){3} is six, and the same nesting is the ReDoS shape
•
(\d)+ leaves only the final repetition in the capture
•
an engine stops a quantified group that matched empty, so it cannot loop forever
•
\b+ is a syntax error or a no-op depending on the flavor
•
JS allows up to 2^32−1; PCRE2 caps a braced count at 65535
•
.*; runs to end of line and walks back to the LAST semicolon
G. Greediness, laziness and possessiveness
•
a quantifier consumes as much as it can, then gives characters back on failure
•
appending ? makes a quantifier take the minimum and grow only when forced
•
one lazy quantifier changes that quantifier, not the rest of the pattern
•
the match is still leftmost-first; lazy only shortens it at that start
•
<.*> swallows to the last > on the line, <.*?> stops at the first
•
<[^>]*> gets the same answer with no backtracking at all
•
*+, ++, ?+ take the maximum and refuse to give any of it back
•
\d++9 can never match, where \d+9 matches 19
•
PCRE2, Java, Ruby and Python's own re from 3.11 have it; JavaScript does not
H. Anchors and input boundaries
•
they assert a position and consume no characters
•
m makes ^ and $ match at every line break, not just the ends
•
Python and PCRE let $ match before a final \n; JS does not
•
both ignore m, but \Z is flavour-split: strict end in Python, while Perl and PCRE2 let it match before a final newline and reserve \z for the strict end; JS has neither
•
\A anchors to the subject start and ignores the multiline flag
•
^…$ is how you say "the entire string", short of fullmatch
•
^a|b anchors only the left branch; the parentheses are load-bearing
I. Word boundaries
•
\b is the zero-width seam between a \w and a non-\w, counting the subject edges
•
\B asserts the two sides are the same kind, word or non-word
•
because ' is not a word character, \b splits don't in two
•
\bcafé\b fails where é is outside the engine's word set
•
\bcat\b also matches beside punctuation, where \scat\s does not
•
[\b] means the backspace character, not a boundary
•
wrapping the term in \b is the fix for renaming cat inside concatenate
J. Alternation and its ordering semantics
•
the first branch that lets the WHOLE pattern succeed is the one taken
•
cat|catalog matches cat even when catalog sits right there
•
^a|b$ parses as (^a)|(b$), not ^(a|b)$
•
(?:…|…) limits the alternation without adding a capture
•
(?:a|) is a roundabout a?, and a common accidental trailing |
•
put the likeliest or cheapest branch first; the engine tries them in order
•
[abc] avoids the branch-and-backtrack that a|b|c pays for
K. Groups: capturing, non-capturing and atomic
•
groups are numbered by the position of their opening parenthesis
•
an enclosing group takes the lower number; the inner one follows
•
index 0 is the whole match, so captures start at 1
•
(?:…) groups for scope without allocating a capture slot
•
a group in an untaken branch yields undefined in JS and None in Python, not ""
•
grouping exists as much to bind a quantifier as to extract text
•
every capture the engine must record is work you pay for on each attempt
•
(?>…) throws away the backtracking positions inside it once it has matched
•
(?>\d+)9 fails on 199 where (?:\d+)9 succeeds
•
(?=(pattern))\1 reproduces atomic behavior where (?>…) is unsupported
L. Named capturing groups
•
(?<name>…) in JS, PCRE2 and .NET; (?P<name>…) in Python
•
a named group still occupies its ordinal number
•
m.groups.name in JS, m.group("name") or m["name"] in Python
•
a name preserves capture identity when another group is inserted and positional indices shift
•
legal only across mutually exclusive alternatives (ES2025), or under a PCRE2 option
•
$<name> in JS and \g<name> in Python's re.sub
M. Backreferences
•
(\w)\1 matches a repeated character, not "another word character"
•
\1 is a backreference in the pattern; $1 is replacement syntax and means nothing there
•
\k<name> in JS and PCRE2, (?P=name) in Python
•
a reference to a non-participating group fails in Python/PCRE but matches empty in JS
•
under i, (\w)\1 matches aA
•
a backreference is why RE2-style linear engines refuse the feature
•
\1 where no group 1 exists reads as an octal escape in legacy JS mode
N. Lookaround
•
(?=…) tests forward and then returns the cursor to where it started
•
(?!…) succeeds precisely when the sub-pattern cannot match here
•
(?<=…) and (?<!…) test the text immediately before the position
•
JS and .NET allow it; Python's re rejects it and PCRE2 bounds it
•
a group inside an assertion keeps its captured text after the assertion ends
•
several lookaheads all test from the same spot, giving independent AND rules
•
^(?=.*\d)(?=.*[a-z]).{8,}$ expresses "must contain" without ordering the classes
•
capturing inside a lookahead finds overlaps a consuming scan would skip
•
one rejects a sequence, the other rejects a single character
•
^(?!.*foo).*$ is the idiom, and why ^[^f]*$ is not the same thing
•
\B(?=(?:\d{3})+$) marks comma positions without consuming a digit
O. Flags and inline modifiers
•
i relaxes character comparison, and under Unicode it does full case folding
•
g changes how the methods iterate, not what the pattern matches
•
m moves ^ and $, and does nothing to .
•
s (JS) or re.DOTALL lets . match a line terminator
•
y requires the match to begin exactly at lastIndex instead of scanning forward
•
x / re.VERBOSE ignores unescaped whitespace and allows # comments; JS has no such flag
•
(?i:…) applies a flag to one span, and (?-i:…) removes it
•
(?i) must be at the start of the pattern in Python
•
u, v, d, y are JS-only; x and a are Python/PCRE
•
JS's d flag adds a start/end offset pair for the match and every capture
P. The backtracking engine
•
the engine explores choices depth-first and rewinds to the last decision on failure
•
backtracking engines return the first successful path; POSIX engines return the longest
•
every quantifier and alternation is a branch, and nesting them multiplies the paths
•
a total failure at index 0 re-runs the whole pattern from index 1
•
engines pre-scan for a required literal and skip start positions that cannot work
•
a leading ^ collapses the start-position loop to a single attempt
•
RE2 and Go's regexp guarantee linear time by dropping backreferences and lookaround
•
some engines cache (position, state) pairs; a plain backtracker does not
•
backreferences and recursion put "regex" outside the regular languages
•
recompiling a pattern inside a loop is the most common regex performance bug
Q. Catastrophic backtracking and ReDoS
•
a quantifier wrapped around an ambiguous quantified atom, as in (a+)+$
•
(a|a)* blows up exponentially; \s*\s*$ degrades quadratically
•
a matching input returns fast; the attack input is the one that ALMOST matches
•
two ways to split the same text is exactly what multiplies the search space
•
one crafted request pins a CPU and a single-threaded runtime stops serving everyone
•
(?:[^"\\]|\\.)* cannot split a character two ways; (?:.|\\.)* can
•
removing the backtracking positions removes the attack, where the flavor allows it
•
cap subject length and impose a match limit or timeout before trusting a pattern
•
escaping stops regex injection; it does nothing about backtracking cost
R. Global and sticky state
•
a g or y regex object stores where its previous match ended
•
a module-level /x/g makes repeated test() calls return true, false, true
•
set it to 0, or construct a fresh regex, before reusing one on new input
•
a regex literal evaluated inside a loop is a new object each time in modern JS
•
a manual exec loop must advance lastIndex itself when the match is empty
•
y is what a tokenizer wants: match here or stop
•
with both, g yields to y's anchoring at lastIndex
•
re keeps no state on the pattern; finditer returns an independent iterator
S. Match results and the matching API
•
the cheapest call when the matched text is not needed
•
element 0 is the match, then the captures, plus index, input and groups
•
String.match returns every matched string under g and DISCARDS the captures
•
every result keeps its captures and its index, and it requires the g flag
•
the offset of the first match, or -1
•
re.match anchors at position 0; re.search scans the whole string
•
requires the entire string to match without bolting on ^ and $
•
Python's findall returns capture groups, not whole matches, as soon as the pattern has groups
•
.start(), .end(), .span() in Python; JS's d flag exposes the same offsets
•
re.compile or a hoisted RegExp, and the small internal cache behind the module functions
T. Replacement syntax
•
replace(/a/, "b") changes only the first occurrence
•
passing a non-global regex to replaceAll throws a TypeError
•
$1, $<name>, and $& for the whole match
•
` $ ` and $'` insert the text before and after the match
•
$$ is how a literal dollar sign survives the replacement parser
•
a non-participating group contributes an empty string, not undefined
•
the replacement string has its own escape rules; \d there is just d
•
match, each capture, offset, whole subject, then the named-groups object
•
a lookup or computation per match, replacing an explicit loop
•
\1 and \g<name>, the count limit, and subn returning the number replaced
U. Splitting
•
splitting on /\s*,\s*/ trims while it separates
•
a capture in the separator is interleaved into the result array
•
JS's limit truncates the array; Python's maxsplit leaves the remainder in the last element
•
an empty separator splits between every unit, with surrogate-pair caveats in JS
•
a separator at the start or end yields an empty string element
•
re.split gained the ability to split on zero-width matches in Python 3.7
•
a quoted comma defeats split(","), which is why a parser exists
V. Python's re module in particular
•
r"\d" stops Python's own string escaping from eating the backslash
•
a bytes pattern cannot be applied to a str, and its escapes stay ASCII
•
re.search caches compiled patterns, so re.compile is about clarity and reuse
•
re.I, re.M, re.S, re.X, re.A, and their inline (?imsxa) equivalents
•
match objects with positions, versus bare strings or tuples
•
conditionals exist but recursion and possessive quantifiers need the third-party regex module
•
if m := re.search(p, s): is the idiomatic test-and-extract
•
alternation of named groups plus finditer is a workable lexer
•
re.DEBUG prints the parsed pattern tree, which localizes a precedence mistake
•
re.error carries the position of the bad construct in the pattern
•
Match.expand applies replacement syntax to a match you already have
W. PCRE-only constructs
•
(?1) or (?&name) re-runs a group's pattern without duplicating it
•
(?R) matches balanced nesting, which no truly regular language can express
•
(?(1)yes|no) branches on whether a group participated
•
\K discards everything matched so far from the reported match
•
\G anchors to the end of the previous match, for contiguous scanning
•
(*SKIP), (*FAIL), (*PRUNE) steer the backtracker explicitly
•
(?|…|…) gives each alternative's groups the same numbers
•
PCRE2_UTF and PCRE2_UCP are what make \w and \b Unicode-aware
•
the match limit and depth limit are the built-in defense against a runaway pattern
X. Using regex well, and when not to
•
whitespace and comments turn a symbol wall into something reviewable
•
two small passes beat one pattern nobody can safely change
•
escape it, or the user is writing your pattern
•
a pattern is only as good as the strings you proved it REJECTS
•
nesting, comments and attribute quoting are decisions a regex cannot make
•
match something permissive and send a confirmation; the RFC grammar is not the goal
•
JSON, CSV, URLs and dates come with parsers that already handle the edges
•
startsWith, includes and a plain split are clearer and faster
•
name the pattern, state what it must reject, and pin an example of each
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.