Regex Cheatsheet - Syntax, Patterns & Templates
Regex is rarely hard because of the metacharacters themselves. It is hard because one requirement can be written several equivalent ways, and the wrong one silently eats too much or too little. A tag-extraction written with .* and the same one written with .*? print identical output on most samples; they diverge only once a line contains a second tag of the same name. Likewise, the difference between and a literal space is invisible until an identifier contains an underscore. This sheet groups character classes, anchors, quantifiers, groups, escaping, practical patterns and traps separately, and every entry states whether it matches a single character, a position, or a whole structure. Reach for it when a pattern passes on hand-picked samples and then misfires on real data, or when grep returns far more lines than expected - both symptoms almost always trace back to greediness or missing anchors. It is deliberately tool-agnostic: unlike grep -E it documents pattern semantics only, and unlike split it handles variable-length structure.
Typical Use Case
For developers who keep losing fights with quantifiers and assertions. The three recurring traps are covered head on — greedy versus lazy matching, lookahead versus lookbehind, and which metacharacters must be escaped in literal use — followed by five ready-to-use patterns for email, URL, phone, date and password. The goal is to be able to judge a pattern correct rather than copy one that happens to run.
Character Classes 10
[abc][^abc][a-z]\d\D\w\W\s\S.Anchors 8
^$\b\B(?=...)(?!...)(?<=...)(?<!...)Quantifiers 8
*+?{n}{n,}{n,m}*?+?Groups & Capturing 6
(abc)(?:abc)(?<name>abc)a|b\1\k<name>Escaping & Flags 7
\\./i/g/m/s/uPractical Patterns 5
[\w.+-]+@[\w-]+\.[\w.-]+https?://[\w./?=&#%-]+1[3-9]\d{9}^\d{4}-\d{2}-\d{2}$^[#\w-]{6,}$Common Pitfalls 4
Greedy vs LazyDot doesn't match newlineEscape special characterscatastrophic backtrackingParameter matrix
| Parameter | Effect | Example |
|---|---|---|
* vs + vs ? | Quantifier shorthand for zero-or-more, one-or-more, and optional exactly once. | a*b / a+b / a?b |
{n,m} | An explicit repetition range, far more predictable than an open-ended quantifier. | ^\d{4}-\d{2}-\d{2}$ |
*? / +? | Lazy quantifiers: match as little as possible so the following pattern can take over. | <.*?> matches the shortest tag only |
^ / $ | Anchor the match to the start or the end of the string. | ^start |
\b | Word boundary, which keeps a pattern from firing in the middle of a longer word. | \bcat\b does not match concatenate |
(?=...) | Positive lookahead: the following text must be there, but it is not consumed. | \d+(?=%) |
(?<=...) | Positive lookbehind: the preceding text must be there, but it is not consumed. | (?<=\$)\d+ |
(?:...) | Non-capturing group. It only bundles alternatives together without storing the match. | (?:foo|bar)+ |
(?<name>...) | Named capture group, so the extracted value can be read back by name instead of index. | (?<year>\d{4})-(?<month>\d{2}) |
/i / /g / /m / /s | Flags for case-insensitive, global, multiline and dotAll behaviour respectively. | str.match(/\d+/g) |
[\w.+-]+@[\w-]+ | A pragmatic email pattern: local part, an @ sign, and a host token. | [\w.+-]+@[\w-]+\.[\w.-]+ |
1[3-9]\d{9} | Eleven-digit mobile phone number format, where the second digit is restricted to 3-9. | 1[3-9]\d{9} |
Common pitfalls
SymptomYou wanted the text inside an HTML tag but the match swallowed the entire line.
Cause<.*> is greedy, so it keeps extending to the last > on the line.
FixSwitch to the lazy form <.*?>, or use [^>]* which simply cannot cross a closing angle bracket.
SymptomThe regex runs extremely slowly or appears to hang on a long string.
CauseNested quantifiers such as (.+)+ trigger exponential backtracking (catastrophic backtracking) when the match eventually fails.
FixAvoid nesting quantifiers; replace them with a negated [^...] class or split the work into two passes.
SymptomThe dot does not cross line breaks, so multi-line text only matches on its first line.
CauseBy default . excludes the newline character.
FixAdd the /s flag, or spell "any character" out as [\s\S].
SymptomSearching for "cat" also returns concatenate and every other word containing it.
CauseThe pattern has no word boundary constraint.
FixConstrain it with \bcat\b.
SymptomSearching for a dot inside a filename matches every character.
CauseA dot is a regex metacharacter and was used literally without escaping.
FixEscape it as \., or use grep -F / re.escape() when you want a purely literal match.
SymptomYou anchored the pattern with ^ and $ but it only took effect at the start and end of the whole text.
CauseThe multiline flag is missing, so ^ and $ bind to the entire input string.
FixAdd the /m flag so they bind to every line boundary.
Troubleshooting
1Confirm what the pattern actually matched
grep -oE "your-pattern" file-o prints only the matched fragment; no output means no hit, so simplify the pattern piece by piece.
2Test whether a word boundary exists at all
grep -nE "\bword\b" fileAdding \b solves most false-positive searches; verify it in isolation before folding it into the main pattern.
3Validate a complex pattern one segment at a time
grep -oE "^(\d{4})-(\d{2})-(\d{2})$" fileTesting small pieces separately pinpoints the failing segment far faster than editing one long pattern.
4Run a regex in JS and inspect the capture groups
node -e "console.log('2026-09-30'.match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/))"match returns an array plus a groups object, and named groups make later extraction self-documenting.
5Diagnose a greedy match that ate the whole line
grep -oE "<[^>]*>" fileReplacing .* with [^>]* is the most direct cure for greediness.
Command Examples
Use a lazy quantifier to capture tag content without swallowing the whole line
const re = /<(\w+)[^>]*>(.*?)<\/\1>/g
const line = '<b>bold</b> and <i>italic</i>'
console.log([...line.matchAll(re)].map(m => m[1] + ':' + m[2]))The lazy (.*?) stops at the first closing tag; a greedy <(.*)> would swallow </b> and <i>italic as content. The \1 backreference keeps the closing tag matched to the opening one.
Output
[ 'b:bold', 'i:italic' ]
Catastrophic backtracking from a greedy nested quantifier
// Danger: nested quantifiers can backtrack exponentially on long strings
const bad = /^(\w+\s?)+$/.test('a'.repeat(30) + '!')
// Safe: one quantifier level, linear match
const good = /^\w+(?:\s+\w+)*$/.test('a'.repeat(30) + '!')A nested quantifier such as (\w+\s?)+ has exponentially many match paths — 30 characters is enough to stall a single test for seconds. The flat \w+(?:\s+\w+)* form stays linear.
Output
bad: false good: false
The hyphen inside a character class must sit at an edge
// Wrong: a-z is parsed as a range running from 'z' down to '8'
const bad = /^[a-z-]+$/.test('-')
// Right: put the hyphen at an edge, or escape it
const ok = /^[-a-z]+$/.test('-')In [a-z-] the trailing hyphen is read as a reversed range from z down to 8, so a plain hyphen string fails validation. Put it first or last, or escape it as \-.
Output
bad: false ok: true
Validate password strength with lookaheads that consume nothing
const re = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/
re.test('Passw0rd')
re.test('password1')The three lookaheads assert digit/lower/upper presence without consuming, so the main .{8,} still measures the full length. Capturing and backtracking instead degrades badly on long passwords.
Output
true false
Named capture groups instead of numeric indexes
const re = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})$/
const m = '2026-07-19'.match(re)
m.groups.y + '/' + m.groups.m + '/' + m.groups.dNamed groups keep field meaning stable when the group order changes, so m[1]/m[2] cannot silently point at the wrong field — a notoriously hard bug in log parsers. Use \k<name> to backreference them.
Output
'2026/07/19'
Common Pitfalls
- Greedy plus nested quantifiers such as
(.+)+or^(a|aa)+$backtrack exponentially — a few dozen characters can pin a worker thread, so pressure-test with the longest real input before shipping. - A hyphen in the middle of a character class is parsed as a range:
[a-z-]silently contains a reversedz-to-8interval and rejects plain hyphens. Place it at an edge or escape it. - GNU and BSD regexes are not interchangeable:
grep -E "\d"fails on BSD/macOS, where you need[[:digit:]];\w,\sand\bare GNU extensions too. - Unescaped metacharacters (
. * + ? ( ) [ ] { } | \ ^ $) are syntax inside a pattern, so password or email validators that forget the backslash silently under- or over-match. .does not match newlines by default, so multi-line extraction quietly loses content. Use thesflag or[\s\S]to cross lines, andmwhen^/$must apply per line.- A
/gregex carrieslastIndexstate, so callingtestorexecrepeatedly on the same object returns alternating results. UseString.prototype.matchAllor build a fresh regex each time.
Tips
- Quantifiers are greedy by default; add ? for lazy (e.g. <.*?> matches the shortest).
- Dot . doesn't match newline; use /s flag or [\s\S] for cross-line.
- Metachars . ^ $ * + ? ( ) [ ] { } | \ / must be escaped with \ when used literally.
- Avoid nested quantifiers like (.+)+; on long strings they can cause exponential backtracking.
FAQ
When should I switch a greedy quantifier to a lazy one, and what is the real difference?
Quantifiers are greedy by default and swallow as much as possible, which is where the trouble starts. The classic pair: <.*> against a line with two tags eats everything between them, while <.*?> stops at the nearest closing bracket and yields a single tag. The test is whether something after the match constrains where it must end — <(\w+?)> or >.*?</div> need the ? because a name must be followed by a bracket or a closing tag. If there is no reliable boundary after the match, lazy matching returns nothing and greedy is the right choice.
My dot matches everything except newlines — how do I make it cross lines?
A dot is defined as "any single character except a newline", so <div>.*</div> only ever matches within one line of multiline text. Two fixes: add the /s (dotAll) flag so the dot also consumes newlines, or replace the dot with [\s\S], which unions whitespace and non-whitespace and therefore covers newlines without any flag. If you only need a bounded number of lines, prefer /m so ^ and $ anchor to every line boundary and pair it with a lazy quantifier rather than enabling /s across the whole file.
Nested quantifiers like (.+)+ freeze the process — how do I spot and rewrite them?
It is backtracking: when an outer repetition fails it must undo and retry the inner one, and nesting makes the number of failing paths grow exponentially. The symptom is a fast return on short input and a pegged CPU or timeout on slightly longer input. Three rewrites: drop the redundant group and use (.+); tighten the repeated unit to an exclusion class such as ([^x]+)+; or avoid regex entirely and split the work into two simple matches. The (?=...) lookahead in this sheet is also useful for validating that something exists without consuming it. Always smoke-test the pattern with a deliberately oversized string.
How do I choose between a capturing group, (?:...) and a named group?
Decide by whether you need the value. A plain (abc) is captured, can be pulled from the result by index, and can be referenced in a replacement as \1. (?:abc) groups purely for composition and consumes no number — use it when a group only exists to bind a quantifier or an alternation, for instance wrapping a two-character quote alternation, which keeps the numbering of the real groups intact. A named group (?<name>abc) is the clearest when there are many parts: both extraction and backreference can then say \k<name>. Rule of thumb: capture only what you read, group everything else with (?:), and upgrade to named groups once indices get confusing.
How do I write format-checking patterns for phone numbers, dates and email without slipping up?
Follow the practical patterns in this sheet: email is [\w.+-]+@[\w-]+\.[\w.-]+, a mainland-China mobile number is 1[3-9]\d{9}, a date is ^\d{4}-\d{2}-\d{2}$, and a password rule is ^[#\w-]{6,}$. Three details decide whether it works. First, anchor the whole string with ^ and $ — without them, 1[3-9]\d{9} happily carves an 11-digit slice out of longer text and passes a bad value. Second, \d covers ASCII digits only, so remember ^\d{4}-\d{2}-\d{2}$ is not "any four digits"; spell the class as [0-9] and consider /u if you need wider coverage. Third, inside a character class . is already literal, so do not escape it as \.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Aug 2, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us