Regular Expressions
Regular expressions (regex) are a compact language for describing text patterns: "an email-shaped string," "a date at the start of a line," "any word repeated twice." One pattern replaces dozens of lines of string-splitting code, and the same core syntax works nearly everywhere — JavaScript, Python, grep, your editor's find-and-replace, Nginx configs, log pipelines.
Regex has a deserved reputation ("now you have two problems") that's really about misuse: patterns that try to parse HTML, validations nobody can read, and catastrophic backtracking taking servers down. Used for what it's good at — finding, validating, and transforming regular text patterns — it's one of the highest-leverage tools a developer carries.
TL;DR
- A regex is a pattern of literals plus metacharacters:
\d(digit),\w(word char),.(any char),[abc](character class),a|b(alternation). - Quantifiers repeat:
*(0+),+(1+),?(0/1),{2,5}(range). They're greedy by default — add?for lazy (.*?). - Anchors pin position:
^start,$end,\bword boundary. Validation without^...$is a classic bug. - Groups
( )capture what matched for extraction/replacement;(?: )groups without capturing;(?<name> )names the capture. - The two production dangers: forgetting to escape user input interpolated into a pattern, and catastrophic backtracking (ReDoS) from nested quantifiers like
(a+)+. - Regex is for regular patterns. HTML, JSON, and nested structures need real parsers — that's not regex's job.
Quick Example
Extracting structure from a log line, in Python and JavaScript:
Named groups ((?<name>...)) turn a cryptic pattern into self-documenting extraction — use them for anything with more than two groups.
Core Syntax
Character Classes
Inside [...], most metacharacters lose their powers — [.+*] matches literal dots, pluses, and stars. Outside, escape them: \. matches a literal dot (example\.com, not "example" + any char + "com").
Quantifiers — and Greed
Quantifiers are greedy: they grab as much as possible while still allowing a match. The classic surprise:
Often better than lazy: a negated class that can't overrun — <[^>]*> ("anything that isn't >") is clearer and faster than <.*?>.
Anchors and Boundaries
For validation, always anchor both ends. (In multiline mode, ^/$ match line starts/ends; \A/\z in Python and ^…$ without m in JS pin the whole string.)
Groups, Alternation, Backreferences
Lookarounds
Zero-width assertions — they check context without consuming it:
Flags
Verbose mode is the underused readability tool: the log-parsing pattern above is legible because it's split across lines.
The Production Pitfalls
Catastrophic Backtracking (ReDoS)
Backtracking engines (JS, Python, Java…) try alternatives when a match fails. Patterns where nested or overlapping quantifiers can split the same text many ways explode exponentially on non-matching input:
An attacker who controls the tested string can freeze your event loop with one crafted input — a real vulnerability class (ReDoS). Defenses:
- Avoid nested quantifiers (
(x+)*,(x*)*) and overlapping alternations ((a|a)*). - Prefer negated classes with a clear exit:
[^"]*instead of.*?inside quotes. - Cap input length before matching; set timeouts where the platform offers them.
- For untrusted input at scale, use a non-backtracking engine: RE2 (linear-time guarantee, used via libraries in most languages) or Rust's
regexcrate.
Unescaped Dynamic Patterns
Interpolating raw user input into a pattern is an injection bug — same instinct as SQL injection: never mix code and untrusted data unescaped.
Using Regex Where a Parser Belongs
HTML, XML, JSON, CSV-with-quoting, nested parentheses — these are not regular languages; regex fundamentally can't track nesting. The Stack Overflow classic about parsing HTML with regex is a joke because it's true. Use the real parser (JSON.parse, an HTML parser, a CSV library) and save regex for the flat, regular fragments inside.
Over-Validating
The perfect RFC 5322 email regex is a page long and still wrong about deliverability. Pragmatic validation is ^[^\s@]+@[^\s@]+\.[^\s@]+$ plus a confirmation email — the only real proof. The same applies to URLs and phone numbers: validate shape loosely, verify semantics elsewhere.
Workflow Tips
- Develop patterns in a tester — regex101.com explains every token, shows match steps (great for spotting backtracking), and supports each engine's dialect.
- Know your dialect: lookbehind support,
\bsemantics on Unicode, named-group syntax, and default Unicode handling differ between JS/PCRE/Python/Go (Go'sregexpis RE2 — no backreferences or lookarounds, by design). - Comment anything non-trivial — verbose mode, or at minimum a comment with example matching input. The person who can't read your regex in six months is you.
- Test the negative cases: patterns are usually broken by what they accidentally match, not what they miss. Property: for validators, feed them your worst production strings.
- In shells:
grep -E(extended) behaves closest to what you expect; remember shell quoting eats backslashes — single-quote your patterns. See Command Line.
FAQ
Is regex worth learning deeply, or just enough to Google?
The 80/20 is real: classes, quantifiers, anchors, groups, greed, and lookahead cover nearly everything you'll write. That 80% is a weekend of practice and pays off for a career — in editors, grep/log spelunking, code, and data cleaning. The dark corners (recursion, atomic groups) can stay Googleable.
Why did my regex work in JavaScript but not in Python/Go?
Dialects. Common trips: Python spells flags as re.I and multiline ^$ needs re.M; Go's RE2 rejects backreferences and lookarounds entirely; older JS lacked lookbehind; \d matches non-ASCII digits in some engines under Unicode flags and not others. Test in the target engine.
match vs search vs findall — which one?
Python: re.match anchors at the start, re.search finds the first occurrence anywhere, re.findall/finditer find all. JavaScript: str.match(re) (all matches with g), re.test(str) for booleans, str.matchAll(re) for iterating groups. Using match when you meant search (or forgetting g) is the everyday bug.
How do I match across multiple lines?
Two separate switches: s (dotall) makes . cross newlines; m (multiline) makes ^/$ match at each line. Needing both is common when carving blocks out of logs or config files.
Are LLMs good at writing regexes?
Genuinely useful for drafting and explaining patterns — but verify against your real inputs (especially adversarial ones) in a tester, and keep the ReDoS rules in mind: generated patterns favor .*? chains that a hostile input can still blow up. Trust, then test.
Related Topics
- Command Line — grep, sed, and awk, where regex lives daily
- JavaScript — RegExp objects and string methods
- Python — The
remodule idioms - SQL Injection — The same untrusted-input instinct
- Logging — Structured logs reduce regex archaeology
- Debugging — Regex testers as micro-debuggers