Regex flashcards: patterns, engines and safety
Recall cards for every regex lesson, from classes, anchors and quantifiers to groups, lookaround, Unicode, flavours, parsers and ReDoS safety.
265 cards. Read the question, answer it in your head, then open the card to check.
-
What does a regular expression describe that a fixed search string cannot?
A whole set of strings that share a shape. ORD-\d\d\d\d stands for ORD, a hyphen and any four digits, so one scan of the log finds every order ID, while a search for ORD-4822 finds only that one number.
From the lesson What a regular expression is and where it runs
-
What does a regex engine give back after it scans a text with a pattern?
Each place where the pattern matches, together with the position at which that match begins.
From the lesson What a regular expression is and where it runs
-
Why can the same match start at index 5 in JavaScript but at byte 6 in Go?
The engines measure positions in different units: JavaScript in UTF-16 code units, Python in code points and Go in UTF-8 bytes. In Rāma राम 123 the letter ā takes two bytes in UTF-8, so Go's count for राम is one higher.
From the lesson What a regular expression is and where it runs
-
Which four flavour families does the regex track run its patterns in?
ECMAScript (JavaScript), Python re, PCRE2 and RE2. They agree on literals and simple classes, and differ in syntax and features once a pattern goes further.
From the lesson What a regular expression is and where it runs
-
Why did ORD-\d\d\d\d report the same six order IDs in JavaScript, Python and Go?
The log is plain ASCII, and on ASCII text the literal letters, the hyphen and \d mean the same characters in every flavour.
From the lesson What a regular expression is and where it runs
-
A fixed word, a shape inside a line, a JSON document: which tool fits each?
A string method such as includes or in for the fixed word, a regex for the shape inside a line, and the format's own parser for JSON, and likewise for HTML, CSV or a URL.
From the lesson What a regular expression is and where it runs
-
In what order does a regex engine try start positions during a search?
Left to right. A failed attempt is forgotten and the engine moves one position on; the first start from which the whole pattern matches gives the result, so a search returns the leftmost match.
From the lesson How an engine finds a match, step by step
-
Why does c[aeiou]t report cat and not cot when it searches scatter cot?
A search stops at the first start position that works, which is cat at position 1. The engine never compares candidates; cot comes from a second search that starts where the first match ended.
From the lesson How an engine finds a match, step by step
-
What is a choice point in a backtracking engine?
A saved place where the pattern offered alternatives the engine has not tried yet. When a later part of the pattern fails, the engine backtracks to it and tries the next alternative.
From the lesson How an engine finds a match, step by step
-
How can (a|ab)c match abc when the alternative a is written first?
a matches, but the c that must follow it meets b, so the attempt fails. The engine backs up to the choice point, tries ab, and then c matches: the first alternative that lets the whole pattern succeed wins.
From the lesson How an engine finds a match, step by step
-
On the text ab, what do leftmost-first and leftmost-longest engines report for a|ab?
Both start at position 0. Leftmost-first returns a, the first match a backtracking search finds; leftmost-longest returns ab, the longest match from that start.
From the lesson How an engine finds a match, step by step
-
How do you make a leftmost-first engine return the longest of several keywords?
Write the longer alternative before any shorter one that is its prefix. The engine takes the first alternative that works, so the long keyword is tried, and wins, first.
From the lesson How an engine finds a match, step by step
-
Why is a search the wrong question for a field that must hold only digits?
A search accepts a match anywhere, so /\d+/.test on 42abc is true. A validator needs a full match: re.fullmatch in Python, or an anchored pattern in JavaScript and Go.
From the lesson How an engine finds a match, step by step
-
Which question does each of Python's re.search, re.match and re.fullmatch answer?
re.search: is there a match anywhere? re.match: is there one at the start? re.fullmatch: does the pattern cover the whole string? For a|ab on ab, re.match gives a while re.fullmatch backs up to ab.
From the lesson How an engine finds a match, step by step
-
Why does the pattern /^12.50$/ also accept 12x50 and 12 50?
The dot is a metacharacter that stands for any one character, not for a full stop. Write \. to match a literal dot.
From the lesson Literals, metacharacters and the dot
-
Which 14 characters are special outside a character class, and what makes one of them literal?
^ $ \ . * + ? ( ) [ ] { } and |. A backslash written before any of them makes it a plain character, so \. matches a dot and \\ matches one backslash.
From the lesson Literals, metacharacters and the dot
-
Which characters does the dot refuse in JavaScript, and which in Python, Go and PCRE2?
JavaScript keeps four line terminators out: LF, CR, LS and PS. Python, Go and PCRE2 keep out only the line feed. A tab is not a line terminator, so the dot always takes it.
From the lesson Literals, metacharacters and the dot
-
How do you let the dot match across line breaks in each flavour?
The s flag in JavaScript, re.S or (?s) in Python, and (?s) in Go and PCRE2. The class [\s\S] does it in all four; [^] works only in JavaScript, and Python rejects it as an unterminated set.
From the lesson Literals, metacharacters and the dot
-
Ruby and JavaScript both have an m flag. Do they mean the same thing?
No. Ruby's m is its dot-all flag, which lets the dot match a line break, while JavaScript's m changes only what ^ and $ match.
From the lesson Literals, metacharacters and the dot
-
What does the JavaScript pattern /\u{938}/ match when the u flag is missing?
938 copies of the letter u. Without the u or v flag the braces are read as a repeat count; with /\u{938}/u the pattern matches the single code point U+0938.
From the lesson Literals, metacharacters and the dot
-
Which code-point escape is spelled the same in JavaScript, Python, PCRE2 and Go?
Only \xhh with two hex digits, such as \x61 for a. Above U+00FF each flavour has its own form: \u{...} with the u flag in JavaScript, \uhhhh or \N{name} in Python, and \x{...} in PCRE2 and Go.
From the lesson Literals, metacharacters and the dot
-
Why does /^.$/ fail on a single emoji such as 👍 in JavaScript?
Without the u flag the dot counts UTF-16 code units, and this emoji needs two of them. With the flag, /^.$/u.test("👍") is true.
From the lesson Literals, metacharacters and the dot
-
Why does new RegExp('\d+') look for the letter d instead of digits?
The string literal is read before the regex parser, and in an ordinary JavaScript string \d is just d, so the parser receives d+. Write '\\d+', or use a regex literal or String.raw.
From the lesson Writing patterns in code: strings and escaping
-
Which ways of writing a pattern pass its backslashes to the regex parser untouched?
A JavaScript regex literal, String.raw, a Python raw string r"..." and a Go string in back quotes. A regex literal still needs a backslash before each slash in the pattern.
From the lesson Writing patterns in code: strings and escaping
-
Why can a pattern built from the ordinary string "\b" never act as a word boundary?
The string turns \b into a backspace character before the regex parser sees it, so the pattern holds no backslash and silently never matches.
From the lesson Writing patterns in code: strings and escaping
-
Which function turns text a person typed into a literal pattern in JavaScript, Python, Go and PHP?
RegExp.escape, re.escape, regexp.QuoteMeta and preg_quote. Their output differs: RegExp.escape even escapes a leading letter and a comma, while QuoteMeta leaves a hyphen and a space as they are.
From the lesson Writing patterns in code: strings and escaping
-
A shopper searches for C++ and the term goes into a pattern unescaped. What happens in each engine?
JavaScript throws and Go rejects the pattern, so the bug shows. CPython and PCRE2 read the two plus signs as repetition of the letter C and quietly return wrong matches, which is far harder to notice.
From the lesson Writing patterns in code: strings and escaping
-
How do you keep a literal dollar sign or backslash in replacement text?
Double the dollar signs in JavaScript and Go, double the backslashes in Python, or pass a function that returns the new text. A pattern escaper is the wrong tool for replacement text.
From the lesson Writing patterns in code: strings and escaping
-
What does \z mean in JavaScript, and what does it mean in Python, Go and PCRE2?
JavaScript has no such escape: without the u flag it is the plain letter z, and in Unicode mode it is a SyntaxError. Python, Go and PCRE2 read it as an anchor for the very end, so the same pattern changes its meaning when it moves between them.
From the lesson Writing patterns in code: strings and escaping
-
How many characters does a character class such as [0-9A-Fa-f] match at a time?
Exactly one, at one position: any one of its members. A six-digit hex colour such as C0FFEE therefore needs the class six times.
From the lesson Character classes, ranges and negation
-
Why does the class [A-z] match 58 characters instead of 52 letters?
A range takes every code point from its first character to its last, and six symbols sit between Z and a. Give each block its own range, [A-Za-z]. A backwards range such as [a-Z] is an error.
From the lesson Character classes, ranges and negation
-
What can the negated class [^0-9] match that the dot does not match by default?
A line break: a class never treats line terminators specially, so [^0-9] takes a newline. It still needs one character, so 7[^0-9] fails at the very end of the text.
From the lesson Character classes, ranges and negation
-
How do you put ], - and ^ inside one class so that every flavour reads them as members?
Escape all three: [\]\-\^]. JavaScript ends a class at the first ] and reads [] as a class that never matches, while Python, PCRE2 and Go take a leading ] as a member.
From the lesson Character classes, ranges and negation
-
Where can you use a POSIX name such as [[:alpha:]]?
In PCRE2 and Go. JavaScript turns it into a wrong class without an error, and Python builds the same wrong class with only a FutureWarning.
From the lesson Character classes, ranges and negation
-
How is the set of letters a to z without the vowels written as one class?
[[a-z]--[aeiou]] in JavaScript with the v flag, (?[ ... ]) in PCRE2 from 10.45, && in Java and a subtraction -[...] in .NET. In Python, Go and PHP 8.5.11 you write the remaining ranges out by hand.
From the lesson Character classes, ranges and negation
-
How many characters does \d match in Python's re, and how many in JavaScript or Go?
760 in Python's str patterns, every decimal digit of Unicode 16.0, against the ten ASCII digits in JavaScript and Go. PCRE2 is ASCII too unless UCP is on.
From the lesson Shorthand classes for digits, words and spaces
-
Which class says you want only ASCII digits, and which says a digit of any script?
[0-9] for the ten ASCII digits and \p{Nd} for a decimal digit of any script. \d means one or the other depending on the engine and its flags.
From the lesson Shorthand classes for digits, words and spaces
-
Does the u flag make JavaScript's \d find Devanagari digits such as १२?
No. JavaScript's \d is 0 to 9 with or without the flag, so /\d+/u finds nothing in पृष्ठ १२. /\p{Nd}+/u takes १२.
From the lesson Shorthand classes for digits, words and spaces
-
Why does \w break a Hindi word such as नमस्ते into pieces or miss it?
The word holds combining marks, and engines treat marks differently: JavaScript and Go find no word at all, Python cuts at every mark, and PCRE2 with UCP or PHP with u cut at the spacing marks.
From the lesson Shorthand classes for digits, words and spaces
-
Which class keeps a word in any script in one piece?
[\p{L}\p{M}], a run of letters and marks, with \p{Nd} for digits and \p{Join_Control} for the joiners. JavaScript needs the u or v flag, or it reads \p as a plain p.
From the lesson Shorthand classes for digits, words and spaces
-
A table row copied from a web page splits on \s+ in Python but stays whole in Go. Why?
Its cells are padded with no-break spaces, U+00A0. \s includes that character in JavaScript, Python and PCRE2 with UCP, but not in Go's RE2 syntax.
From the lesson Shorthand classes for digits, words and spaces
-
How do you choose Unicode-aware or ASCII-only shorthand classes on purpose?
re.ASCII in Python, UCP in PCRE2, the u modifier in PHP and UNICODE_CHARACTER_CLASS in Java. JavaScript and Go have no such switch, so there you write the class out.
From the lesson Shorthand classes for digits, words and spaces
-
What does an anchor such as ^ or $ match?
A position, not a character. By default ^ holds at the start of the text and $ at its end, and the match each one makes is empty.
From the lesson Anchors: start, end and multiline mode
-
In which engines does ^abc$ accept abc followed by a line feed?
Python, PCRE2, PHP, Java and Perl, where $ also holds before one final line break, and Ruby, where ^ and $ always work line by line. JavaScript and Go refuse it, because there $ means only the very end of the text.
From the lesson Anchors: start, end and multiline mode
-
Why does re.match(r"^[0-9]{6}$", pin) accept a PIN code followed by a line feed?
In Python $ also holds just before a final line feed, so a seventh character gets through a six-digit rule. re.fullmatch(r"[0-9]{6}", pin) refuses it.
From the lesson Anchors: start, end and multiline mode
-
How does a validator ask for the whole text and nothing more in each language?
^...$ without the m flag in JavaScript and Go, fullmatch or \A...\z in Python, and \A...\z in PCRE2, PHP, Java and Ruby.
From the lesson Anchors: start, end and multiline mode
-
Why should a validator never end with \Z when the pattern may move between engines?
Python reads \Z as the very end, but PCRE2, PHP, Perl and Ruby also let it hold before one final line feed, Java before a final line terminator, and Go does not support it at all. End with \z or the language's whole-match call.
From the lesson Anchors: start, end and multiline mode
-
What does multiline mode change about the anchors, and what does it leave alone?
It makes ^ hold after every line break and $ before every one (the m flag in JavaScript, re.M in Python, (?m) in PCRE2 and Go). \A and \z keep meaning the start and end of the whole text.
From the lesson Anchors: start, end and multiline mode
-
How do an HTML pattern attribute and a JSON Schema pattern differ in anchoring?
The browser wraps an HTML pattern in ^(?: and )$, so it must match the whole value. A JSON Schema pattern is not anchored, so its author has to write ^ and $ by hand.
From the lesson Anchors: start, end and multiline mode
-
Why does the JSON Schema pattern ^cat|dog$ accept the value catalog?
The bar splits the anchors apart: ^ belongs to cat and $ to dog, so the pattern means cat at the start or dog at the end. Put a group around the alternatives first: ^(?:cat|dog)$.
From the lesson Anchors: start, end and multiline mode
-
Where does \b match, and how many characters does it add to a match?
At a position where one neighbour is a word character and the other is not, the start and end of the text counting as non-word. It adds none; \B matches at every other position.
From the lesson Word boundaries and whole-word search
-
Why does \bRam\b find the Ram in Ram-Lal but not in Ramesh or Ram_2?
The hyphen is not a word character, so a boundary falls between Ram and the hyphen. In Ramesh the next character is a letter and in Ram_2 it is the underscore, both word characters, so no boundary falls after Ram.
From the lesson Word boundaries and whole-word search
-
Why does \bcaf\b find caf inside café in JavaScript and Go?
\b is only as Unicode-aware as the engine's \w. In JavaScript, Go and PCRE2 without UCP the letter é is not a word character, so a boundary falls before it. Python and PHP with u keep café whole.
From the lesson Word boundaries and whole-word search
-
Does adding the u flag make JavaScript's \bराम\b find the Hindi word राम?
No. The u and v flags change how a pattern reads code points, not which characters are word characters, so the pattern still finds nothing. Test the neighbours of each match yourself.
From the lesson Word boundaries and whole-word search
-
When does \b stop meaning a word boundary?
Inside a class, where [\b] is the backspace U+0008, and in an ordinary string, which turns \b into a backspace before the engine reads it. That is why re.search("\bcat\b", line) without the r prefix finds nothing.
From the lesson Word boundaries and whole-word search
-
How do you search for a whole word reliably in any script?
Escape the term, then keep a match only when neither the character before nor the one after is a letter, combining mark, decimal digit, connector punctuation or joiner. Use lookaround in JavaScript, Python and PHP, and a check in code in Go.
From the lesson Word boundaries and whole-word search
-
Which part of a pattern does a quantifier repeat?
Only the one atom right before it: a character, the dot, an escape, a class or a group. bana+ repeats the last a, while ba(?:na)+ repeats the pair na.
From the lesson Repetition with *, +, ? and {m,n}
-
Which range of counts does each of *, +, ? and the brace forms allow?
* allows 0 or more, + 1 or more and ? 0 or 1. Braces write any other range: {n} exactly n, {n,} at least n, {n,m} from n to m.
From the lesson Repetition with *, +, ? and {m,n}
-
Why does \d{6} return 900001 when it searches a line holding the phone number 9000012345?
A count fixes how many repetitions, not where the run stops, so six digits inside a longer number match too. Use \b\d{6}\b to search, or anchors such as ^\d{6}$ to check a field.
From the lesson Repetition with *, +, ? and {m,n}
-
Why is ba[na]+ the wrong way to repeat the syllable na?
A class matches one character from a set, so [na]+ takes n and a in any order and also accepts banaan and baaaa. ba(?:na)+ repeats the two characters in their order.
From the lesson Repetition with *, +, ? and {m,n}
-
Does go*al match gopal?
No. In a regex the star repeats the atom before it, so go*al allows only more o's between g and al. A shell's star means any string; in a regex write go.*al, or go[a-z]*al.
From the lesson Repetition with *, +, ? and {m,n}
-
Which way of writing a brace count means the same in every flavour of the track?
Both bounds and no spaces, such as x{0,3}, with counts of at most 1,000. x{,3} is plain text in Go and in JavaScript without a flag, and Go refuses any count above 1,000.
From the lesson Repetition with *, +, ? and {m,n}
-
Why does line.match(/\d*/g) return empty strings among the numbers?
\d* can match nothing, so a global search reports an empty match at each position without a digit. Collect runs with \d+, which needs at least one digit.
From the lesson Repetition with *, +, ? and {m,n}
-
Why does ".*" return one long match on the row "Asha","Pune",411001?
The star is greedy: it takes everything and gives characters back from the end only as far as the closing quote needs, so the match runs to the last quote it can reach and holds both fields.
From the lesson Greedy and lazy quantifiers
-
What does the lazy pattern ".*?" return on the same row, and why?
"Asha" and then "Pune". A lazy quantifier tries the rest of the pattern before each extra repetition, so it stops at the first closing quote that lets the whole pattern match.
From the lesson Greedy and lazy quantifiers
-
Does a lazy quantifier return the shortest possible match?
No. Laziness decides how much a match takes, never where it starts, and the leftmost start wins first: x.*?y on xxy returns xxy, not xy.
From the lesson Greedy and lazy quantifiers
-
Why does /".*?/g return lone quotes instead of whole fields?
Nothing follows .*? in the pattern, so it is content with zero characters. Say where the field ends, /".*?"/g, and the closing quote makes it extend.
From the lesson Greedy and lazy quantifiers
-
Why is "[^"]*" often a better field pattern than ".*?"?
The class cannot run past the closing quote or cross a line break, so it stays right where the lazy dot goes wrong. On a 1,000-character field PCRE2 needs a match limit of 2 for it against 1,003 for the lazy dot.
From the lesson Greedy and lazy quantifiers
-
What does (?U) do, and how do you port a pattern written for it?
In Go, PCRE2 and PHP it swaps greedy and lazy. It does not compile in JavaScript or Python and means Unicode classes in Java, so add a question mark to each quantifier without one and remove it from each that has one.
From the lesson Greedy and lazy quantifiers
-
What does the second plus sign in \d++ change?
It makes the repetition final: \d++ takes the same digits as \d+ but keeps no shorter run to fall back to, so if the rest of the pattern fails, the engine gives up at that start instead of handing digits back.
From the lesson Possessive quantifiers and atomic groups
-
Why does a*+a find nothing in aaa, while a+a matches all three characters?
a*+ keeps all three a's and gives none back, so the last a in the pattern has nothing left to match. The plain repetition gives one a back.
From the lesson Possessive quantifiers and atomic groups
-
Why does (?>in|inch)es refuse inches while (?>inch|in)es accepts it?
An atomic group throws away its saved positions once it matches. In the first, in matches, es then fails on ch, and inch is never tried; in the second, inch matches first and es follows.
From the lesson Possessive quantifiers and atomic groups
-
How much work did two possessive plus signs save on the list pattern ^(?:\d+,?)+$?
To refuse twenty digits followed by a stray character, PCRE2 needed 1,572,864 turns of its matching loop for the plain pattern and 4 for the possessive one, and both accepted every valid list.
From the lesson Possessive quantifiers and atomic groups
-
When can a repetition be locked without changing what the pattern accepts?
When the characters after it are outside what it matches. "[^"]*+" keeps its answers, while ".*+" matches no quoted field, because the dot also takes the closing quote. Confirm the rewrite on a list of lines.
From the lesson Possessive quantifiers and atomic groups
-
Which quantifier does PCRE2 make possessive by itself, and which can it not?
It possessifies \d+ in \d+x, because x is not a digit, cutting the cost from 41 turns to 2. It cannot do so for \s+$, because a newline is white space.
From the lesson Possessive quantifiers and atomic groups
-
JavaScript has no possessive quantifier. How do you get the same lock there?
With a lookahead and a backreference, (?=(X))\1. It adds a capture group, which shifts every later numbered group by one. Go needs no lock, because it never re-tries a path.
From the lesson Possessive quantifiers and atomic groups
-
In a match of (\d{4})-(\d{2})-(\d{2}), what is group 0 and where does your own numbering start?
Group 0 is the whole match, the full date the pattern found, and it exists even in a pattern without brackets. The groups you write are numbered from 1, so the year is group 1, not group 0.
From the lesson Capturing and non-capturing groups
-
How are the groups of ((\d{4})-(\d{2}))-(\d{2}) numbered?
By their opening parentheses, read from the left: the outer bracket is group 1, the year group 2, the month group 3 and the day group 4. A bracket added near the front shifts every later number.
From the lesson Capturing and non-capturing groups
-
What does a non-capturing group (?:...) give you that a plain group does not?
It groups part of a pattern for a quantifier or a choice without filling a slot, so the numbers of the groups you read stay where they were.
From the lesson Capturing and non-capturing groups
-
What does a repeated group such as (\d)+ keep after it matches several digits?
Only the text of its last repetition, a single digit. To keep the whole run, put the quantifier inside the group: (\d+).
From the lesson Capturing and non-capturing groups
-
What does a group that took no part in the match give back?
Depending on the language, undefined, None, null or an empty string with a -1 index. That is not the same as an empty match, so test whether the group took part before you use its text.
From the lesson Capturing and non-capturing groups
-
How do capturing groups change a split, and how does JavaScript report where each group matched?
A split keeps every separator that sits inside a capturing group, so the separators appear among the pieces. JavaScript's d flag reads back the position of each group.
From the lesson Capturing and non-capturing groups
-
Which engines read (?<name>...) and which read (?P<name>...)?
(?<name>...) compiles in JavaScript, Go, PHP and PCRE2, and (?P<name>...) in Python, Go, PHP and PCRE2. Only Go, PHP and PCRE2 read both, so a pattern shared by Python and JavaScript needs its own form on each side.
From the lesson Named groups across regex flavours
-
How do you read a named group year in JavaScript, Python, Go and PHP?
m.groups.year in JavaScript, m["year"] or m.group("year") in Python, SubexpIndex("year") and then that slot of the match in Go, and $m["year"] in PHP.
From the lesson Named groups across regex flavours
-
How is a named group written inside a replacement template?
$<name> in JavaScript, \g<name> in Python, and ${name} in Go, PCRE2's substitute and Java's Matcher.
From the lesson Named groups across regex flavours
-
Why does PHP's preg_replace copy ${day} into the output instead of the day?
Its replacement template has no reference by name: the manual lists only the numbered forms. Use preg_replace_callback with a function that reads $m["day"], or a numbered reference such as $1.
From the lesson Named groups across regex flavours
-
What does each engine do when two alternatives use the same group name?
JavaScript allows it since ES2025, with its own slot per alternative and one property in m.groups. PCRE2 allows it with (?J), Go compiles it and maps the name to the leftmost slot, and Python refuses the pattern.
From the lesson Named groups across regex flavours
-
How do you avoid a duplicate-name pattern when a field can take one of several values?
Put the alternation inside the one named group, (?<kind>cash|card|upi), spelled (?P<kind>cash|card|upi) for Python, so every engine reads the same single slot.
From the lesson Named groups across regex flavours
-
Why does the validator ^cat|dog$ accept hotdog?
The bar is the lowest-precedence operator, so it splits the whole pattern: ^cat or dog$. hotdog ends in dog. ^(?:cat|dog)$ puts the anchors around both alternatives and accepts only cat and dog.
From the lesson Alternation and the order of choices
-
Why does the highlighter pattern if|in|int|integer|interface paint only part of integer?
A leftmost-first engine tries alternatives in the order they are listed and keeps the first that fits, so in matches before int or integer is tried. List longer options first, or guard the alternation with \b or an anchor.
From the lesson Alternation and the order of choices
-
Which engines return the longest alternative whatever order the pattern lists them in?
POSIX regular expressions, Go's regexp.CompilePOSIX and re2js with LONGEST_MATCH: they take the longest match from the earliest start. V8, QuickJS, CPython's re and PCRE2 keep the first alternative that fits.
From the lesson Alternation and the order of choices
-
Why is a character class better than an alternation of single characters?
A class is one atom that records no choice point, it shows the whole set at a glance, and it keeps clear of slow shapes such as (a|aa)+.
From the lesson Alternation and the order of choices
-
How do you make a quantifier such as + apply to a whole alternation?
Wrap the alternatives in a group first, (?:cat|dog)+, so that the plus repeats the whole choice.
From the lesson Alternation and the order of choices
-
How does \b(\w+)\s+\1\b find the doubled word in the the quick fox?
Group 1 captures a word, and \1 asks the engine to find exactly the same text again after the white space. \b\w+\b alone matches every word, repeated or not.
From the lesson Backreferences: matching the same text again
-
How does a backreference such as \1 differ from $1 in a replacement string?
\1 runs during the search and can fail: (\w+)\s+\1 finds nothing in foo bar. $1 runs after the search, copying the captured text into the output, so it cannot fail.
From the lesson Backreferences: matching the same text again
-
Which spellings refer back to a captured group inside a pattern?
\1 by number, \k<name> in V8 and PCRE2, (?P=name) in CPython and PCRE2, and the relative form \g{-1} in PCRE2 and PHP.
From the lesson Backreferences: matching the same text again
-
What does (a)?b\1 do when it meets the text b?
Group 1 never ran. V8 and QuickJS match the unset backreference as the empty string, so b matches; CPython, PCRE2 and PHP let the pattern fail; Go refuses to compile it.
From the lesson Backreferences: matching the same text again
-
Why do RE2, Go and Rust's regex crate reject backreferences, and what do you do instead?
They keep their linear-time guarantee, which backreferences would break. Find the candidates with a regex and compare the captured texts in code.
From the lesson Backreferences: matching the same text again
-
Can a backreference check that nested tags are closed correctly?
No. It can tie a closer to its opener for one straight pair, but it cannot track how deep tags are nested, so a parser in code handles anything beyond a single pair.
From the lesson Backreferences: matching the same text again
-
What does \d+(?=%) return on the text 50% of 200, and what does Go say about it?
50. The lookahead (?=%) checks that a per-cent sign comes next without matching it. Go rejects the pattern outright with invalid or unsupported Perl syntax, because it has no lookaround.
From the lesson Lookahead: conditions without consuming text
-
What does it mean that a lookahead is zero-width?
It matches a position, not a character: it tests the text ahead and consumes nothing, so the engine stays where it was and the characters it inspected are still there for the rest of the pattern.
From the lesson Lookahead: conditions without consuming text
-
On 50% of 200 and 42 and 99% then 300, what do \d+(?=%) and \d+(?![\d%]) return?
The positive lookahead keeps the numbers followed by a per-cent sign, 50 and 99. The negative one keeps those followed by neither a digit nor a per-cent sign: 200, 42 and 300.
From the lesson Lookahead: conditions without consuming text
-
How can one pattern check several independent rules, and where should you not use the trick?
Stack lookaheads at one position: none of them consumes text, so each tests the string from the same place. Do not use it to force password composition rules, which NIST SP 800-63B advises against.
From the lesson Lookahead: conditions without consuming text
-
How do you find the keyword var everywhere except inside quoted strings, with no lookaround?
Match a quoted string as one alternative and capture the keyword as the other, "[^"]*"|\b(var)\b, then keep only the matches whose group 1 is set. It works even in Go.
From the lesson Lookahead: conditions without consuming text
-
Why do Go and RE2 refuse (?= and (?!, and how do you check several rules there?
Leaving lookaround out keeps their matching linear in time. Join several plain patterns with &&, use re2js's RE2Set, or write a small parser.
From the lesson Lookahead: conditions without consuming text
-
What is the match of (?<=rs)\d+ on rs500 and $20?
500. The lookbehind tests that rs stands just before the position and consumes nothing, so the match is the text the digits find, not rs500.
From the lesson Lookbehind and its limits in each flavour
-
Which engines accept (?<=rs|USD)\d+, and which accept (?<=a+)b?
V8, QuickJS, PCRE2 and Java accept the two branches of different widths, and CPython refuses them. Only V8, QuickJS and Java still accept the unbounded a+.
From the lesson Lookbehind and its limits in each flavour
-
What does fixed width mean for a lookbehind in CPython's re?
Every text the lookbehind can match has the same length, so the compiler knows how far to step back. (?<=rs|cd) is two characters on either branch and compiles; (?<=rs|USD) is two or three and is refused.
From the lesson Lookbehind and its limits in each flavour
-
How long may a lookbehind be in each engine?
CPython wants a fixed width. PCRE2 since 10.43, and PHP with its PCRE2 10.44, accept bounded variable-length branches up to 255 characters. V8, QuickJS and Java set no limit.
From the lesson Lookbehind and its limits in each flavour
-
Why do both PCRE2 and Python refuse (?<=\s+)Rs, and what works in both?
The quantifier gives the lookbehind no upper length, which both engines refuse. (?<=\s)Rs, a one-character lookbehind, runs in both, as does capturing the prefix and reading group 1.
From the lesson Lookbehind and its limits in each flavour
-
What does \K do, and which engines run it?
It resets the start of the reported match, so foo\Kbar returns bar. PCRE2, PHP and Perl run it. CPython and Go reject it, and JavaScript reads it as the letter K without the u or v flag and rejects it with them, so a port needs a capture group or a fixed-width lookbehind.
From the lesson Lookbehind and its limits in each flavour
-
What do you do with a lookbehind in Go, and since which Safari version does JavaScript lookbehind work?
Go has none, so match the prefix too and read the value from a capture group, the one fix that works in every flavour. Safari supports lookbehind from 16.4, so browser code needs a known minimum version or a fallback.
From the lesson Lookbehind and its limits in each flavour
-
What does the conditional (?(1)yes|no) match, and which engines run it?
Its yes part when group 1 took part in the match so far, its no part otherwise. CPython and PCRE2 run it; JavaScript, Go and Java 25 refuse it, so portable code tests the group after a plain match.
From the lesson Conditionals, recursion and PCRE2 extras
-
Which engines match balanced brackets with \((?:[^()]++|(?R))*\)?
PCRE2 and PHP. CPython stops at (?R) with unknown extension, Java 25 calls it an unknown inline modifier, and JavaScript and Go fail earlier, at the possessive ++.
From the lesson Conditionals, recursion and PCRE2 extras
-
Why does the recursive bracket pattern use [^()]++ rather than [^()]+?
On one unclosed bracket with 16 letters, the greedy form cost 753,665 match calls, because each run of letters can be split many ways. The possessive form reads each run one way only and needed 22.
From the lesson Conditionals, recursion and PCRE2 extras
-
What does the branch reset group (?|(cat)|(dog)) do?
It gives every alternative the same group numbers, so the animal is always in group 1. Outside PCRE2 the portable form reads whichever group is set.
From the lesson Conditionals, recursion and PCRE2 extras
-
What does (*SKIP)(*FAIL) do in a PCRE2 pattern?
It consumes the text matched before it and never reports it, so, for example, words inside quotes are skipped. Engines without verbs get the same result from an extra group and code that keeps only the matches that set it.
From the lesson Conditionals, recursion and PCRE2 extras
-
What do the start-of-pattern items (*UCP) and (*LIMIT_MATCH=n) change?
They set options for the whole match: (*UCP) makes \d take every decimal digit, and (*LIMIT_MATCH=n) caps the work, so a runaway match stops with error -47.
From the lesson Conditionals, recursion and PCRE2 extras
-
When should a recursive bracket pattern give way to a short parser?
When the brackets come in several kinds, when an error must say where it is, or when the code must run outside PCRE2, PHP and Perl. A loop with a stack runs in every language and names the bracket that is wrong.
From the lesson Conditionals, recursion and PCRE2 extras
-
In how many places can a regex flag be set, depending on the flavour?
Up to five: a letter after the slash (/a/i), a flags argument (re.compile(p, re.I) or new RegExp(p, "i")), a global inline group at the start ((?i)...), a scoped group around one part ((?i:...)), and, in PCRE2's C library, an option of the compile call.
From the lesson Flags and inline modifiers
-
What does a scoped modifier such as (?i:error) do, and does it capture?
It turns the flag on for that part only, and (?-i:...) turns it off. It never captures: on ERROR: disk, /(?i:error): (\w+)/ puts the code in group 1. Wrap it, ((?i:error)), when you need the word too.
From the lesson Flags and inline modifiers
-
Where is a global (?i) group refused?
JavaScript rejects it as a SyntaxError, though it accepts the scoped (?i:...) since ES2025. Python 3.11 and later refuse the global group with an error unless it stands at the very start of the pattern.
From the lesson Flags and inline modifiers
-
How do you make a Go pattern case-insensitive?
With an inline group in the pattern, such as (?i) at the start or a scoped (?i:...), because Go's regexp has no flags argument at all.
From the lesson Flags and inline modifiers
-
Why can a validator that reuses /\d{6}/g answer true, false, true for the same code?
A regex with the g or y flag keeps lastIndex, so each call to test starts where the previous match ended. Drop the flag, or set lastIndex to 0 before every call.
From the lesson Flags and inline modifiers
-
What happens when matchAll gets a regex without the g flag?
It throws a TypeError, so that mistake is loud. With g it works on a copy of the regex, so the original's lastIndex never moves.
From the lesson Flags and inline modifiers
-
What must change when a Ruby pattern with the m flag moves to another flavour?
At least two letters: Ruby's m is dot-all, which is s elsewhere, and Ruby's ^ and $ always hold at every line, which elsewhere needs the m flag.
From the lesson Flags and inline modifiers
-
What does free-spacing mode change about a pattern, and what does it leave alone?
It changes how the pattern is written: the engine drops spaces and line breaks and reads each hash as the start of a comment. What the pattern matches stays exactly the same.
From the lesson Readable patterns: free-spacing and comments
-
What turns free-spacing mode on in Python, PCRE2 and PHP, Java and .NET?
re.VERBOSE or (?x) in Python, the x modifier in PCRE2 and PHP, COMMENTS in Java and IgnorePatternWhitespace in .NET.
From the lesson Readable patterns: free-spacing and comments
-
Why does (\d+) (kg|g) stop matching 5 kg once it is compiled with re.X?
In free-spacing mode the space is layout, so the pattern becomes (\d+)(kg|g). Write the space with a backslash, (\d+)\ (kg|g), or as a class, (\d+)[ ](kg|g). A hash that belongs to the pattern needs the same care.
From the lesson Readable patterns: free-spacing and comments
-
JavaScript and Go have no free-spacing mode. How do you comment a long pattern there?
Keep the pattern in pieces in your own code, each with its comment, and hand the engine the joined string. Both engines also refuse the (?#...) comment.
From the lesson Readable patterns: free-spacing and comments
-
Which three slips spoil a JavaScript pattern composed from named parts?
Inserting a regex object directly, which brings its slashes along (take .source); joining alternatives without a group, so GET|POST next to the path lets GET fish through; and inserting plain text unescaped, so the dot of web-1.pune accepts web-1Xpune.
From the lesson Readable patterns: free-spacing and comments
-
What documentation of a pattern cannot go out of date?
A table of lines that must match and lines that must not, kept beside the pattern and run on every change.
From the lesson Readable patterns: free-spacing and comments
-
Why is 👍🏽 four long in JavaScript, two in Python and one to a reader?
It is two code points, the thumbs-up and a skin-tone modifier, both above U+FFFF. JavaScript counts UTF-16 code units, two per code point; Python counts code points; a reader sees one grapheme cluster.
-
What should a character limit count when it is shown to people?
Grapheme clusters, for example with Intl.Segmenter: the message Thanks 👍🏽! has a length of 12 in JavaScript but nine graphemes. A limit that protects storage counts the unit stored, such as UTF-8 bytes.
-
What does the u flag change in a JavaScript pattern, and what does it leave alone?
The pattern reads code points instead of code units, accepts \u{...} and folds case the Unicode way, and needless escapes become SyntaxErrors. Match positions are still counted in code units.
-
Why can ^café$ refuse a café that looks identical, and what fixes it?
One spelling stores é as a single precomposed code point, the other as a plain e with the combining acute, U+0301, after it. Bring the text and the word to the same normalisation form, usually NFC, before you match.
-
Under ignore-case, can the Kelvin sign match k, and can ß match SS?
Yes for the Kelvin sign, U+212A: its simple case fold is k, so ignore-case pairs them (in JavaScript only with the u flag). No for ß and SS in JavaScript, Python, Go, PHP and PCRE2, because ignore-case compares simple folds, one code point to one; only full case folding, such as the regex package's FULLCASE flag, pairs them.
-
Why does /^[a-z]+$/iu accept a word that is not plain ASCII?
Unicode case folding pairs the Kelvin sign with k and ſ with s, so Kerala spelt with a Kelvin sign passes. Drop i and write [A-Za-z], or refuse the cross-over with r in PHP, (?r) in PCRE2 or re.ASCII in Python.
-
Why does a Python bytes pattern such as rb'\w+' cut Hindi words apart?
It sees each UTF-8 byte as one character, so its classes, its dot and its ignore-case stop at ASCII. Decode the text first and match it with a str pattern.
-
Why does /\p{L}+/gv split the Hindi word राम into र and म?
Its vowel sign is a combining mark of category M, not a letter of category L, so the match stops there and the next one starts at the following consonant. [\p{L}\p{M}]+ keeps the word in one match.
-
How do you match Tamil letters by script in JavaScript, Go and PCRE2?
JavaScript needs Script= or sc= before the name, \p{Script=Tamil}; Go takes only the short form \p{Tamil}; PCRE2 and the regex module take both, and PCRE2 reads the short form as Script_Extensions.
-
How many grapheme clusters do the three tools find in नमस्ते?
Intl.Segmenter and the regex module's \X follow the Indic conjunct rule of UAX 29 and find three. PCRE2's \X splits more finely and finds four, so a slice at its boundary can cut a conjunct.
-
What does JavaScript's v flag add that u does not have?
Properties of strings such as \p{RGI_Emoji}, which can match a sequence of code points, and the \q{...} syntax. /\p{RGI_Emoji}/u is a SyntaxError; write /\p{RGI_Emoji}/v.
-
Can Python's built-in re match \p{L} or \X?
No, it refuses both. The third-party regex module adds them, or you can build a class from unicodedata.category.
-
Why can a newly encoded character match in one engine and not in another?
Each engine ships its own Unicode data: Node 24.21.0, Go 1.27.1 and PCRE2 10.49 know 17.0, Python 3.14.8's tables 16.0.0, PHP 8.5.11's PCRE2 stops before 16.0, and the regex module knows 18.0. Test such characters on every engine you ship.
-
What does replacing the group of (a) with $1x give on the text ab in JavaScript, PHP and Go?
axb in JavaScript and PHP. Go gives b: it reads $1x as a group named 1x, finds none and writes nothing, without an error. Python treats the dollar form as plain text.
From the lesson Flavours compared: porting a pattern safely
-
How do you write group 1 followed by the letter x in a Go and in a Python replacement?
${1}x in Go, and \g<1>x or \1x in Python. A template is read by the engine you move to, so copy it only after you rewrite it.
From the lesson Flavours compared: porting a pattern safely
-
What are the two ways a ported pattern can fail, and which one costs more?
The new engine can refuse the pattern, which costs one error message, or compile it and quietly mean something else, which costs a wrong answer in production. A clean compile proves only the grammar.
From the lesson Flavours compared: porting a pattern safely
-
Why is a{,2} a quiet trap when a pattern moves between Python and JavaScript?
Both engines compile it, but Python reads it as a count, up to two a's, while JavaScript without a flag reads the same five characters as literal text.
From the lesson Flavours compared: porting a pattern safely
-
Which rows of the flavour matrix break ports most often?
The dollar sign before a final newline, \Z and \z, {,n}, the spelling of named groups, lookbehind limits, replacing empty matches, and split with captured separators.
From the lesson Flavours compared: porting a pattern safely
-
What are the five steps of the porting checklist?
List every construct, look each one up, rewrite the smallest part, run the same strings in both engines, and compare. Every difference sends you back to the look-up.
From the lesson Flavours compared: porting a pattern safely
-
What proves a port, and what are the limits of that proof?
A differential test: one table of pattern, subject and expected result, run in both engines. It proves the port only on the strings it holds, so each row the pattern touches needs a string that lands on its difference.
From the lesson Flavours compared: porting a pattern safely
-
Which JavaScript call answers which question about a pattern and a string?
test gives yes or no, search a position, exec and matchAll match objects with positions and groups, match with g only the matched texts, replace and replaceAll a new string, and split the pieces.
From the lesson JavaScript regex techniques for text jobs
-
How do the g and y flags differ when no match starts at lastIndex?
A g pattern searches forward from lastIndex and skips whatever does not match. A y pattern must match exactly at lastIndex, so it returns null and resets lastIndex to 0. With both flags it behaves as y.
From the lesson JavaScript regex techniques for text jobs
-
Why can a lexer built from sticky rules report where a query stops making sense?
Every character must belong to some rule, so the scan halts at the first character no rule reads, and that position is the error to report. Set each rule's lastIndex just before every exec, because a failed match resets it to 0.
From the lesson JavaScript regex techniques for text jobs
-
Which JavaScript calls move, reset, copy or ignore lastIndex?
exec and test move it; match and replace with g set it to 0; matchAll copies it; search and split leave it alone. Only patterns with g or y have this state.
From the lesson JavaScript regex techniques for text jobs
-
When does a replacement need a function instead of a replacement string?
A string can only copy pieces of the match, such as $&, a group number or $<name>. A function receives the match, the groups, the position and the input, so it can compute or look up the new text, or leave a match as it was.
From the lesson JavaScript regex techniques for text jobs
-
How does the d flag help a highlighter keep every character of the text?
It reports the start and end of the match and of every group, so the code can slice the original text around each hit and keep every space and tab between them.
From the lesson JavaScript regex techniques for text jobs
-
What does RegExp.escape return for the typed text a.b*c?
\x61\.b\*c, a pattern that matches exactly those five characters and nothing else, which is what text a reader typed must become before it joins a pattern.
From the lesson JavaScript regex techniques for text jobs
-
Why scan records with one pattern and re.finditer rather than one findall per field?
Each match carries every field of one record, and a missing field is None in its own record. Separate lists pair fields only by position, so one missing reference shifts every record after it.
From the lesson Python regex techniques and the regex module
-
What does re.sub do when the replacement is a function?
It calls the function once per match, left to right, with the match object, and the str it returns takes the match's place: re.sub(r'\d+', lambda m: str(int(m[0]) * 2), 'a3 b4') gives a6 b8. Returning an int raises TypeError.
From the lesson Python regex techniques and the regex module
-
Why is int(float(amount.replace(',', '')) * 100) wrong for converting an amount to paise?
Float arithmetic turns 19.99 into 1998 and 2.30 into 229. Capture the whole part and the fraction as two groups and join their digits before a single int().
From the lesson Python regex techniques and the regex module
-
What does re.split add to its result when the pattern has a capturing group?
Each captured separator, between the pieces it separated; a group that took no part adds None. Empty matches count too: re.sub(r'a*', '-', 'baac') gives -b--c-.
From the lesson Python regex techniques and the regex module
-
How should count, maxsplit and flags be passed to re.sub, re.subn and re.split?
By keyword, such as flags=re.I or maxsplit=1. Since Python 3.13, passing them by position is deprecated and raises a warning that names the parameter.
From the lesson Python regex techniques and the regex module
-
What does Python's re lack that the third-party regex module adds?
A match timeout, \p{...} properties and \X for grapheme clusters, plus fuzzy matching. Adding the module is a dependency decision your project makes.
From the lesson Python regex techniques and the regex module
-
What do PHP, pcre2test and Perl share, and what differs between them?
PHP and the pcre2test and pcre2grep programs share PCRE2's pattern language in different releases (10.44 in PHP 8.5.11, 10.49 for the programs), and Perl runs its own engine that PCRE2 imitates. Options and replacements belong to each door.
From the lesson Regex in PCRE2, PHP and Perl
-
Which delimiter suits a PHP pattern for paths such as /learn/regex/, and where do modifiers go?
A character the pattern does not contain, such as #^/learn/regex/#, or a pair of braces, so no slash needs a backslash. Modifier letters follow the closing delimiter, as in #^/learn/regex/#i.
From the lesson Regex in PCRE2, PHP and Perl
-
Why does /^\d{6}$/ accept a postal code followed by a newline in PHP, and what fixes it?
PCRE2's $ also holds before a final newline. The D modifier, /^\d{6}$/D, makes $ hold only at the true end of the text.
From the lesson Regex in PCRE2, PHP and Perl
-
What does the u modifier change for a PHP pattern?
PCRE2 reads the pattern and the text as UTF-8, and \d takes Unicode digits too, so without u a Devanagari digit such as ७ is skipped.
From the lesson Regex in PCRE2, PHP and Perl
-
How should you test what preg_match returned?
With === false. It returns 1, 0 or false, and only false means the call failed: read a compile problem in the warning and a run problem in preg_last_error_msg().
From the lesson Regex in PCRE2, PHP and Perl
-
What may a preg_replace replacement contain, and where do case escapes and names work?
Only \n, $n and ${n}. PCRE2 with substitute_extended and Perl also read case escapes and group names in the replacement; in PHP, preg_replace_callback does that work.
From the lesson Regex in PCRE2, PHP and Perl
-
What does PCRE2's match limit do when a pattern runs away?
It counts matching steps and turns the runaway match into an error: false in PHP and error -47 in pcre2test. The cure is a pattern without a repeat inside a repeat.
From the lesson Regex in PCRE2, PHP and Perl
-
What does Go's regexp promise about match time, and what does it refuse in exchange?
Every match is bounded by the compiled pattern's size times the text's length, because it follows all paths at once. It refuses backreferences, lookaround, possessive and atomic forms and counts above 1,000 when the pattern compiles.
From the lesson Regex in Go, RE2 and re2js
-
What does Go's error invalid named capture: (?<=a) really point at?
A lookbehind. Go's parser reads (?< as the start of a named group, so it complains about the name. Rewrite that part with a capture group or a check in code.
From the lesson Regex in Go, RE2 and re2js
-
What does Go's FindAllStringSubmatchIndex return for each match?
Byte offsets in pairs: group k starts at place 2k and ends at place 2k + 1, with -1 for a group that took no part.
From the lesson Regex in Go, RE2 and re2js
-
Why does the Go template $1xxxx delete text without any error?
$name takes the longest name it can, so Go looks for a group named 1xxxx and writes nothing. Write ${1}xxxx, use $$ for a dollar sign, or send the text through ReplaceAllLiteralString or ReplaceAllStringFunc.
From the lesson Regex in Go, RE2 and re2js
-
Why does replacing a* with a hyphen give -b-c- on baac in Go but -b--c- in JavaScript and Python?
Go ignores an empty match that starts where the previous match ended. Written with + instead of *, the same job gives one answer in all three.
From the lesson Regex in Go, RE2 and re2js
-
What do CompilePOSIX, Longest and (?U) change in Go?
CompilePOSIX and Longest switch to leftmost-longest matching, and (?U) makes quantifiers lazy by default. They change which text matches, never the time bound. In Java the same (?U) means Unicode character classes.
From the lesson Regex in Go, RE2 and re2js
-
What does re2js carry into the browser, and what does a pattern shared with Go still need?
RE2's syntax and its linear bound, with JavaScript-style replacement strings. A pattern shared with Go still needs a differential test of its outputs.
From the lesson Regex in Go, RE2 and re2js
-
Why does \h+ pull 0f out of xyz 0f in Ruby but trim spaces in Java and PCRE2?
Ruby reads \h as a hexadecimal digit, while Java and PCRE2 read it as horizontal white space. The same pattern compiles everywhere and means different things, so port behaviour, not syntax.
From the lesson Regex in Java, .NET, Ruby and Rust
-
How do Java's matches and find differ, and what else surprises a port to Java?
matches tests the whole input and find searches a part, so find lets xAB12y pass a check for AB12. split drops trailing empty fields unless its limit is negative, and \w stays ASCII until UNICODE_CHARACTER_CLASS is set.
From the lesson Regex in Java, .NET, Ruby and Rust
-
What does .NET's RegexOptions.NonBacktracking guarantee, and what does it refuse?
Linear time. In exchange it refuses lookaround, backreferences and more: compiling (?<=ERR-)\d+ with it throws. Timeouts and source generation change the cost of a match, never its result.
From the lesson Regex in Java, .NET, Ruby and Rust
-
Why is /^foo$/m in Ruby the wrong way to get per-line anchors?
Ruby's ^ and $ are line anchors already, and its m flag only lets the dot cross line breaks. For the whole string write \A and \z.
From the lesson Regex in Java, .NET, Ruby and Rust
-
What do Regexp.timeout and Regexp.linear_time? tell you in Ruby?
Regexp.timeout is nil by default, so no time cap applies until you set one. Regexp.linear_time? reports whether a pattern avoids the features that can run slowly.
From the lesson Regex in Java, .NET, Ruby and Rust
-
What does Rust's regex crate guarantee, and what does fancy-regex trade it for?
The regex crate guarantees linear time by leaving out lookaround and backreferences. fancy-regex adds them on a backtracking engine that can run exponentially: the same guarantee-or-features choice Go's RE2 makes.
From the lesson Regex in Java, .NET, Ruby and Rust
-
How do you choose a safety net for a pattern you do not fully trust?
A linear-time engine ends the risk but refuses some features, a timeout caps the damage after the fact, and a review trusts the pattern but not the input. Pick by whether pattern and input are trusted and whether you can change the platform.
From the lesson Regex in Java, .NET, Ruby and Rust
-
Why did grep count 6 requests from 198.51.100.2 when only 2 came from that address?
Each dot of the address matches any character, and 198.51.100.2 is also the start of 198.51.100.23. With -F the text is read literally and with -w only whole words count, and the count drops to 2.
From the lesson grep, sed and awk: regex on the command line
-
How do grep, grep -E and grep -F each read a pattern?
Plain grep reads basic syntax, where +, ?, |, braces and parentheses are characters unless a backslash comes first. grep -E reads extended syntax, where they are operators. grep -F reads no regex at all: every character is literal.
From the lesson grep, sed and awk: regex on the command line
-
Why does sed 's/ \+/ /g' squeeze runs of spaces with GNU sed but change nothing with the Mac's sed?
\+ is an extension: GNU sed reads it as one or more, while the Mac's sed looks for a space followed by a plus sign and finds none, without an error. sed -E 's/ +/ /g' works the same in both.
From the lesson grep, sed and awk: regex on the command line
-
Why can grep and a JavaScript program count different matches for the alternation /cart|/cart/items?
grep, sed and awk keep the longest of the matches that start earliest, while PCRE2, JavaScript and Python keep the alternative written first. Put the longer alternative first, or make the extra part optional, and both rules agree.
From the lesson grep, sed and awk: regex on the command line
-
What do sed's \3 and & become in JavaScript and Python, and how is an in-place edit written?
$3 and $& in JavaScript, \3 and \g<0> in Python. sed's g flag changes every match on a line, and an in-place edit is sed -i on GNU sed but sed -i '' on the Mac's.
From the lesson grep, sed and awk: regex on the command line
-
Why should a portable grep -E search write [0-9] instead of \d?
\d is not POSIX. The Mac's grep read it as a digit, while GNU grep warned about a stray backslash, looked for the letter d and counted 0. [0-9] means a digit to both.
From the lesson grep, sed and awk: regex on the command line
-
What do awk's match(), sub(), gsub() and split() report?
match() sets RSTART and RLENGTH to where the leftmost-longest match starts and how long it is; sub(), gsub() and split() return counts that can serve as tests. Beyond ASCII, gawk counts characters where the Mac's awk counts bytes.
From the lesson grep, sed and awk: regex on the command line
-
Which command-line tools can run a pattern with lookaround?
Those built on PCRE2: GNU grep -P, pcre2grep and ripgrep -P. The Mac's grep refuses -P, so a search that must run everywhere selects lines with -E and does the rest with cut or awk.
From the lesson grep, sed and awk: regex on the command line
-
Why does (?<=PIN[ :])[1-9][0-9]{5} work in Excel but fail in Google Sheets?
Excel's REGEX functions use PCRE2, which has lookbehind, while Sheets uses RE2, which refuses it. In Sheets, match the label too and keep the digits in a group: REGEXEXTRACT with one group returns that group's text.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
Which regex engine sits behind Excel, Google's data products, PostgreSQL, MySQL and SQLite?
PCRE2 for Excel, RE2 for Google Sheets, BigQuery and the analytics filters, PostgreSQL's own advanced regular expressions, ICU for MySQL, and for SQLite whatever regexp() function the host program adds.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
How do you make a Google Sheets regex ignore case?
Start the pattern with (?i). Sheets has no case argument, while Excel's REGEX functions take a case_sensitivity argument.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
How do GA4's matches regex and Search Console's custom regex filter differ?
GA4 tests the whole value, as if the pattern were wrapped in ^(?:...)$, and is case sensitive. Search Console matches anywhere and ignores case. Add .* to make GA4 search, ^...$ to make Search Console anchor, and (?-i) to make it case sensitive.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
Why does name ~ '\bAsha\b' find no rows in PostgreSQL?
In PostgreSQL \b means a backspace, which the names never hold. Write \mAsha\M for a word boundary on each side, or \yAsha\y for either side.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
Which PostgreSQL regex functions arrived in version 15?
regexp_count, regexp_like, regexp_substr and regexp_instr.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
What does REGEXP give you in MySQL 8.4 and in SQLite out of the box?
MySQL's REGEXP functions use ICU, so Unicode property escapes work. SQLite's REGEXP operator is an error until the host program registers a regexp() function, and that function's flavour is the host's choice.
From the lesson Regex in spreadsheets, SQL and analytics tools
-
Why does pattern="[A-Za-z0-9_-]+" let every value through a username field?
The browser compiles the attribute with the v flag, where that hyphen before ] is an error, and then drops the pattern with at most a console message. Write [A-Za-z0-9_\-]+ and test a value that must fail.
From the lesson Regex in HTML, JSON Schema and config files
-
Why does an HTML pattern attribute need no ^ and $?
The browser wraps it as ^(?: and )$ before testing, so it always checks the whole value. A pattern that carries its own anchors is simply anchored twice.
From the lesson Regex in HTML, JSON Schema and config files
-
Why does a JSON Schema pattern pass a value that only contains a match?
The keyword uses the ECMA-262 dialect and is not implicitly anchored, so it searches. A schema that means the whole value adds ^ and $, and a maxLength.
From the lesson Regex in HTML, JSON Schema and config files
-
Why does the line .*\.log$ in .gitignore ignore none of your log files?
Git reads it as a glob, not a regex: the first dot is a dot, * is the wildcard, \. an escaped dot and $ a dollar sign. Write *.log, or logs/*.log for one folder.
From the lesson Regex in HTML, JSON Schema and config files
-
Which special characters may a robots.txt rule use, and which rule wins?
Only * as a wildcard and $ at the end, so Disallow: /*.pdf$ blocks every PDF. The longest matching rule wins, and Allow beats Disallow on a tie.
From the lesson Regex in HTML, JSON Schema and config files
-
Which engine reads regex locations and redirects in nginx and Apache, and which two slips does it let through?
PCRE. The slips are an unescaped dot and a pattern without anchors; before writing any pattern, ask which engine reads it and whether it matches the whole value or searches.
From the lesson Regex in HTML, JSON Schema and config files
-
In what order should a text-cleaning pipeline run its steps?
Normalise to NFC, delete invisible characters, collapse spaces, unify dashes and quotes, and extract last, so that the extracting pattern meets one spelling of everything.
-
Why does NFC normalisation come before every other cleaning step?
Each later step compares code points, and a visible é has two spellings: one precomposed code point, or a plain e with a combining mark after it. NFC turns canonically equivalent spellings into one; normalising at the end comes after the matching is done.
-
Why delete invisible characters before collapsing runs of spaces?
A zero width space between two spaces splits them into two runs, so two spaces are left behind once it is deleted afterwards.
-
Why should a cleaning job spell out its class of spaces instead of using \s?
\s holds a different set in Python, JavaScript, Go and PCRE2, and none holds the zero width space U+200B. Spell out Unicode's White_Space list and delete invisible characters in a step of their own.
-
What should a phone-number cleaner do with a number its rule refuses?
Set it aside. Strip the separators, match the whole remainder against one written rule, rewrite what passes in one format, and never cut a number down to its last ten digits: a guessed number looks exactly like a real one later.
-
Why is float(text.replace(",", "")) a poor way to read grouped amounts?
It accepts 1,2,34,567 and 45,6789.00, which no grouping writes, and floats drift in a sum. Match the whole value against one grouping's anchored pattern first, then convert it to whole hundredths.
-
Should the pattern for a day-first date also decide whether that day exists?
No. The pattern finds and rewrites the shape, with a backreference that keeps both separators alike; a date parser, or a round trip through Date.UTC, decides whether the day exists.
-
When should a CSV, JSON or date parser take over from a regex in a cleaning job?
As soon as fields can be quoted across lines, values can nest, or a value must exist rather than only look right. Until then, named groups and one scan with finditer or matchAll turn key=value lines into records.
-
Why did ^[A-Z]{4}0[A-Z0-9]{6}$ under Python's re let a 12-character IFSC into the database?
In Python $ also holds before a final newline, so eleven valid characters plus a newline matched. Use re.fullmatch or \A and \z, and cap the length before the pattern runs.
From the lesson Validating formats: numbers, IDs and dates
-
Why is re.search the wrong call for a validator, even with a correct pattern?
search is satisfied by a match anywhere, so foo SBIN0000691 bar passes. re.fullmatch has to cover every character of the value.
From the lesson Validating formats: numbers, IDs and dates
-
When should a numeric range move out of the pattern and into code?
A short range such as 0 to 255 still fits in an alternation of four branches. For a wide one such as 1 to 65535, keep the shape as the regex and compare the number in code.
From the lesson Validating formats: numbers, IDs and dates
-
What do the official definitions of an IFSC and a PIN code give a pattern to follow?
An IFSC is four letters, the digit zero and six letters or digits; a PIN code is a zone digit followed by five more digits. A shape check still cannot know whether that branch or PIN code exists.
From the lesson Validating formats: numbers, IDs and dates
-
Why is a check digit such as Luhn's computed in code rather than in the pattern?
It catches a single mistyped digit and most swaps of neighbouring digits, which every shape accepts, and it is a short loop run after the shape passes. A pattern for it could exist in principle, but nobody could read it closely enough to trust it.
From the lesson Validating formats: numbers, IDs and dates
-
Why does ^\d{6}$ accept १२३४५६ in Python, and what should an ID pattern use?
Python's \d matches decimal digits of every script by default, while JavaScript's and Go's are ASCII only. Write [0-9]{6}, which reads the same in every engine and under every flag, or (?a:\d{6}) for Python's ASCII meaning.
From the lesson Validating formats: numbers, IDs and dates
-
How do you validate an IPv4 address or a calendar date in Python?
Check the shape with a pattern, then let the ipaddress or datetime module decide whether the value really exists.
From the lesson Validating formats: numbers, IDs and dates
-
Which rule should a form use to check the shape of an email address?
The HTML Standard's valid email address rule, which departs from RFC 5322 on purpose. On 20 invented addresses it accepted 9, where \S+@\S+ accepted 18 and a typical hand-written pattern 7.
From the lesson Emails, URLs and when to use a parser
-
Why can no regular expression prove that an email address works?
A pattern reads only the characters, so it shows the shape of an address and nothing about the mailbox. A made-up address passes every shape check; send a confirmation link and count the address as real once the link is used.
From the lesson Emails, URLs and when to use a parser
-
What host do a browser's URL parser and Python's urllib.parse read in https://pay.test\@shop.example/cart?
The browser's parser reads pay.test and urllib.parse reads shop.example. RFC 3986's regular expression only splits a URL into parts; a parser decides which host it names, and parsers can disagree.
From the lesson Emails, URLs and when to use a parser
-
How should a URL allowlist decide whether a link is allowed?
Parse once, refuse input that parsers could read differently, compare the parsed scheme, host and port with the allowed set, and pass on the URL rebuilt from the checked parts, not the text you were given.
From the lesson Emails, URLs and when to use a parser
-
Why is url.startswith("https://img.shop.example") a poor allowlist check?
In the lesson's run it let through fetches from two other hosts and refused the allowed host written in capitals. Look up the parsed hostname, which is already in lower case, in a set of allowed hosts.
From the lesson Emails, URLs and when to use a parser
-
Why can no regular expression find where a nested div ends?
The end depends on a count that grows with the depth, and a regular expression has a fixed amount of memory. A lazy pattern stops at the inner closing tag; a parser that counts depth finds the outer one.
From the lesson Emails, URLs and when to use a parser
-
How do a loose pattern and a strict check share the work of finding addresses in text?
The loose pattern is a pre-filter: it must miss nothing and decides nothing. The strict rule or parser after it keeps only what it can vouch for, and is only as good as that rule: the file name logo@2x.png passes the HTML email rule.
From the lesson Emails, URLs and when to use a parser
-
How many steps does (a+)+ take on 16 letters a and ! in a backtracking search, and in a state-set search?
327,676 steps one path at a time, where a step is one visit to one state, against 65 when the search carries a set of states. One more letter roughly doubles the first count and adds 4 to the second.
From the lesson Backtracking engines versus automata engines
-
What makes a language regular?
A finite automaton can check it in one pass with a fixed number of states. A DFA is in one state at a time and an NFA may be in several, but both recognise the same languages.
From the lesson Backtracking engines versus automata engines
-
How large is the NFA that Thompson's construction builds from a pattern?
Each character or operator gets at most one state and each group none, so the NFA grows in step with the pattern, whatever text it reads later.
From the lesson Backtracking engines versus automata engines
-
What is the worst case of a backtracking engine, and of a state-set simulation, on (a+)+ with n letters and !?
Backtracking tries every path on a text that cannot match: 5 × 2ⁿ − 4 steps. The state-set simulation reads each character once, so its work is at most the NFA's size times the text's length: 4n + 1 steps.
From the lesson Backtracking engines versus automata engines
-
Why do automaton engines refuse backreferences, and why do they leave out lookaround?
A backreference needs memory that grows with the text, which takes the pattern outside the regular languages. Lookaround is left out for what it would cost, not for what it can say.
From the lesson Backtracking engines versus automata engines
-
Why does RE2 build its DFA lazily instead of all at once?
A DFA reads each character with one lookup but can need 2ⁿ⁺¹ states where the NFA needs n + 2. RE2 builds states as it needs them inside a fixed memory budget and falls back to the NFA simulation when the cache stops helping.
From the lesson Backtracking engines versus automata engines
-
Is (a+)+ slow on long texts?
Not on a long run of a's, where its first path matches. It is slow on a run that cannot match, because every way of grouping the letters is then tried, so test such patterns on texts that almost match.
From the lesson Backtracking engines versus automata engines
-
How did PCRE2's work on ^(a+)+$ grow as the input of a's and one x got longer?
It needed 655,360 turns of its matching loop to refuse eighteen a's and an x, and every four more characters multiplied the count by sixteen.
From the lesson Catastrophic backtracking and ReDoS
-
What are the four shapes of catastrophic backtracking?
A nested quantifier such as (a+)+, overlapping alternatives inside a repetition such as (a|aa)+, adjacent repetitions that want the same characters such as \d+\d+, and an end-anchored run tried from every start, such as \s+$.
From the lesson Catastrophic backtracking and ReDoS
-
What do the four slow shapes have in common, and how does their work grow?
Each lets one subject be split among the pattern's atoms in many ways, all reaching the same failure, and every way is tried. Nested and overlapping shapes grow exponentially with the input; adjacent and end-anchored ones grow polynomially.
From the lesson Catastrophic backtracking and ReDoS
-
Why can one slow match deny service to a whole Node.js server?
The match holds the single thread that serves every other request on that worker, so one worst-case input starves them all. OWASP lists this as ReDoS, and CWE-1333 names the weakness.
From the lesson Catastrophic backtracking and ReDoS
-
What caused the 2016 Stack Overflow outage, and what fixed it?
A trim pattern of the \s+$ kind met a post with about 20,000 consecutive whitespace characters, at every home page view. The site failed its load balancer's health check and was unavailable for 34 minutes; a substring function replaced the regex.
From the lesson Catastrophic backtracking and ReDoS
-
How do you reproduce a suspected slow pattern without hanging your own editor?
Run it under a limit, not a stopwatch: V8's experimental fallback flag, PCRE2's match limit or a per-call timeout in Python's regex module, or use a short input the growth cannot blow up on.
From the lesson Catastrophic backtracking and ReDoS
-
Why is one static ReDoS checker not enough?
Each has gaps: recheck leaves a backreference undecided, ESLint's recommended rules miss \s+$, and neither ESLint rule set reports ^(a|aa)+$.
From the lesson Catastrophic backtracking and ReDoS
-
How much did making the hyphen required save in the service-name rule ^[a-z]+(?:-?[a-z]+)*$?
To refuse sixteen letters and a wrong character, PCRE2 needed 131,072 turns of its matching loop for that rule and 34 for ^[a-z]+(?:-[a-z]+)*$, which accepts and refuses exactly the same names.
From the lesson Writing fast regexes and measuring them
-
What makes a pattern slow, and what is the first fix?
The same text can be divided between its parts in many ways, and a failing match has to try them all. Leave one way of reading the text, and the work stops growing with the length of the input.
From the lesson Writing fast regexes and measuring them
-
What must a rewritten pattern pass before you time it?
A table of lines that must match and lines that must not: a rewrite counts only if every answer survives it.
From the lesson Writing fast regexes and measuring them
-
What is the unrolled pattern for a quoted field with backslash escapes, and what does it cost?
"[^"\\]*(?:\\.[^"\\]*)*": one item for the run of ordinary characters and one for each escape. It stayed at 3 turns at any field length, while the alternation grew to 36 turns at sixteen characters.
From the lesson Writing fast regexes and measuring them
-
Why does ".*?" cut short a quoted field whose message contains \"?
A lazy quantifier only changes the order in which lengths are tried; it does not say what may not appear inside the field, so it stops at the escaped quote. Only a negated class, as in the unrolled pattern, says that.
From the lesson Writing fast regexes and measuring them
-
Why drop a leading .* and anchor the pattern?
The engine can then use what it knows about the first character and the shortest possible subject: 1 turn instead of 42 on a line without the field.
From the lesson Writing fast regexes and measuring them
-
Does PCRE2's just-in-time compiler fix a pattern whose work explodes?
No. It gives the same answers from machine code, but the unrewritten rule still multiplied its work by sixteen every four characters. A faster engine changes the constant; a rewrite changes how the work grows.
From the lesson Writing fast regexes and measuring them
-
What should a regex benchmark report so that its number means something?
A ratio against a named input on a named engine, measured on matching, failing and worst-case input, with a deterministic count beside it. Compile the rule once and reuse it, and use a string method when the text is fixed.
From the lesson Writing fast regexes and measuring them
-
What are the two ways untrusted regex reaches a service?
A user supplies the pattern, as in a search box, a filter builder or alert rules, or a user supplies the input your own pattern runs on, such as a form field, a header or an upload. Each needs its own defence.
From the lesson Regex with untrusted patterns and input
-
Why is compiling a pattern a visitor typed like running their code?
On a backtracking engine a small pattern can block the thread for a very long time: Node's default engine was still running ^(a+)+$ on 34 a's and ! after three seconds. A user pattern needs a time bound, not just a successful compile.
From the lesson Regex with untrusted patterns and input
-
What does a linear-time engine guarantee for user patterns, and what does it refuse?
Its time stays linear in the text whatever the pattern, which removes the explosive case. It always refuses backreferences, and lookaround too in RE2, Go, Rust and .NET's non-backtracking mode.
From the lesson Regex with untrusted patterns and input
-
What does a deadline worker or a match timeout do, and what does it not do?
It caps the damage rather than preventing it: the engine still spends the time up to the limit. Pair the deadline with an input length cap and a safe pattern where you can.
From the lesson Regex with untrusted patterns and input
-
Does an input length cap make a slow pattern cheap?
No. It shrinks the worst case for any pattern by bounding how much text a match can meet, but the pattern itself stays as slow as it was.
From the lesson Regex with untrusted patterns and input
-
How do you match a user's text literally, and why keep the engine patched?
Escape it with RegExp.escape, re.escape, QuoteMeta, preg_quote or Pattern.quote. Keep every bundled copy of the engine patched as well, because the engine's own bugs become your service's bugs.
From the lesson Regex with untrusted patterns and input
-
In what order does a safe search feature stack its defences?
Cheapest first: cap the length, then escape the text or compile the pattern on a linear engine, then run under a deadline, so every request ends in an answer or a clear error rather than a blocked thread.
From the lesson Regex with untrusted patterns and input
-
A pasted [A-Z]{2}-\d{6} came with one matching example. What did ten test strings show?
It got five wrong in a browser and six in Python, accepting the code with a letter or a seventh digit added, a space in front, a newline after it or a second code beside it. One example says nothing about what a validator must refuse.
From the lesson Reviewing regexes from forums or AI assistants
-
What are the seven checks for a regex you did not write?
The flavour it was written for, the anchors, the classes, overlapping runs, a static checker, the replacement template, and a table of near misses.
From the lesson Reviewing regexes from forums or AI assistants
-
What does reading a pattern token by token find before any text is matched?
The defects visible in the pattern itself: a search pattern doing a validator's job, a shorthand wider than the rule, and a repeated group whose body repeats. It needs no engine, input or test data.
From the lesson Reviewing regexes from forums or AI assistants
-
Why must the expected answers of a test table come from the rule written in words?
Rows recorded by running the pattern all pass, so the table proves only that the pattern agrees with itself. A row you are unsure of is a question for whoever owns the field, not a row to delete.
From the lesson Reviewing regexes from forums or AI assistants
-
Why should a pasted pattern run in every engine the team ships?
One pattern decides differently in each: two of the ten rows changed their answer in Python, and in PHP the end anchor accepted a trailing newline while one modifier widened the digit class as well.
From the lesson Reviewing regexes from forums or AI assistants
-
What does a static checker settle in a regex review, and what does it leave open?
Only the cost question. Two checkers disagree often enough that one verdict is not a review, and only the test table says whether the pattern decides what the field really means.
From the lesson Reviewing regexes from forums or AI assistants
-
What should a regex review leave beside the pattern?
The test table and a three-line note: what the field holds, which engines read it, and the construct rejected on purpose.
From the lesson Reviewing regexes from forums or AI assistants