Lesson 2 of 9 · 20m
Quantifiers, Classes, and Anchors: Saying Exactly What You Mean
Greedy, lazy, and possessive quantifiers, why negated classes beat lazy dots, and the anchors that turn 'contains' into 'is'.
Most broken regexes aren't wrong, they're vague. .* is the regex equivalent of "I couldn't be bothered to describe this," and vagueness is where both bugs and slowness come from. I once reviewed a log-redaction rule, token=.*, that had been happily deleting every field after the token on every line for six months. Nobody noticed because the lines still looked redacted. The fix was token=[^&\s]+ — say what a token is, not what it isn't bounded by.
Greedy, lazy, possessive
Every quantifier — *, +, ?, {n,m} — comes in up to three modes:
- Greedy (
.*,\d+): take as much as possible, give back on failure. The default everywhere. - Lazy (
.*?,\d+?): take as little as possible, extend on failure. - Possessive (
.*+,\d++) and atomic groups ((?>...)): take as much as possible and never give it back. PCRE, Java, and Python 3.11+ support them; JavaScript does not.
Lazy is not a performance fix and not a correctness fix — it just changes which match the search finds first. Possessive and atomic constructs are a performance tool: they delete bookmarks, so a doomed match fails fast instead of retrying every split.
Negated classes beat lazy dots
const url = "/checkout?session=9f8c2e1a&step=payment#card";
console.log(url.match(/session=(.*)&/)[1]); // 9f8c2e1a works... until a second & appears
console.log(url.match(/session=([^&#]*)/)[1]); // 9f8c2e1a says exactly what a value is[^&#]* encodes the grammar: a query value runs until & or #. It's correct on every input and the engine can't backtrack into it in any interesting way.
Anchors turn "contains" into "is"
const zip = /\d{5}/;
console.log(zip.test("90210")); // true
console.log(zip.test("UPS 1Z999AA10123456784")); // true (five digits somewhere)
const strictZip = /^\d{5}(?:-\d{4})?$/;
console.log(strictZip.test("90210-1234")); // true
console.log(strictZip.test("UPS 1Z999AA10123456784")); // falseAn unanchored validation regex is a "contains" check. That's the single most common regex bug in code review. \b is the other anchor worth knowing: a zero-width boundary between a word character and a non-word character, so \bERROR\b matches ERROR: but not TERRORIST.
Classes worth having in muscle memory
[^"] anything but a quote (includes newlines!)
[ \t] horizontal whitespace only; \s also matches \n and \r
[\w.-] hyphen last so it's literal; dot is literal inside a class
[[:digit:]] POSIX class, works in grep/sed/awk where \d doesn't
\d{1,3} bounded repetition: cheaper and more honest than \d+Field notes
[A-z]is a bug: betweenZandain ASCII sit[ \ ] ^ _and the backtick. Write[A-Za-z].\s+across a whole file buffer will happily swallow newlines and join records. Use[ \t]+when you mean "spaces on this line."- In Python
x{,5}means 0 to 5; in JavaScript (withoutu) it's the literal text{,5}. Always write{0,5}. - Bound everything you can.
\d{1,10}documents intent and caps the work;\d+invites a megabyte of digits.