Regular expressions: the 20% you'll use 80% of the time
10 Oct 2026 · Text · 6 min read
Regular expressions have the worst reputation in programming, and it's mostly undeserved. Yes, a full regex engine is a small language of its own, and yes, some patterns look like a cat walked across the keyboard. But you don't need the whole language. A small handful of ideas — literals, character classes, quantifiers, anchors, groups — handles the overwhelming majority of real text jobs: finding dates in a log, validating a form field, renaming a thousand files, pulling email addresses out of a spreadsheet column.
This article is that handful. No automata theory, no exhaustive reference. Just the patterns you'll actually reach for, with examples you can test as you read. If a pattern below looks useful, try it in your editor's find box — nearly every code editor, from VS Code to Sublime to vim, speaks the same basic dialect.
Start with the literal: text matches itself
The first thing to know is also the most reassuring: ordinary characters in a regex match themselves. The pattern cat matches the three letters "cat" wherever they appear — inside "cat", inside "concatenate", inside "certificate". There's no magic until you ask for it.
Two characters are magic from the start. The dot . matches any single character except a newline, so c.t matches "cat", "cut", "c3t" and "c t". And the backslash \ removes the magic: \. matches a literal full stop, which is what you want when you're hunting for the end of a sentence rather than any character at all. A dozen or so characters are special in regex — . ^ $ * + ? { } [ ] \ | ( ) — and the rule is simple: if you mean the character itself, put a backslash in front of it.
Character classes: matching a set of characters
Square brackets define a set: [aeiou] matches any single vowel. Ranges use a hyphen: [a-z] matches any lowercase letter, [0-9] any digit, [A-Za-z] any letter at all. Put a caret first and the set inverts: [^0-9] matches any single character that is not a digit.
Because these sets come up constantly, regex gives you shorthands:
| Shorthand | Means | Opposite |
|---|---|---|
| \d | Any digit (0–9) | \D — anything but a digit |
| \w | Word character: letters, digits, underscore | \W — anything else |
| \s | Whitespace: space, tab, newline | \S — anything but whitespace |
So \d\d\d matches any three digits in a row — a pattern you'll see again in the greatest-hits table below. Note that \w includes the underscore and digits, which surprises people matching "words" in prose; [A-Za-z] is the stricter choice when you mean letters only.
Quantifiers: saying how many
Real text is repetitive, and quantifiers are how you say "more of that":
- * — zero or more. ab*c matches "ac", "abc", "abbbbc".
- + — one or more. \d+ matches "7", "42", "1000000" — any whole number.
- ? — zero or one (optional). https? matches "http" and "https".
- {n} — exactly n. \d{4} matches a four-digit year.
- {n,m} — between n and m. \w{3,12} matches a username of 3 to 12 word characters.
The troublemaker is *, because "zero or more" includes zero: the pattern a* matches the empty string at every position, which is why beginners get confusing match counts. When you mean "at least one", reach for + instead — \d+ for numbers, \s+ for gaps between words, .+ for "the rest of the line". As a rule of thumb, + is the quantifier you'll use ten times more often than *.
Anchors: saying where in the line
So far every pattern could match anywhere. Anchors pin it down: ^ means "start of the line" and $ means "end of the line". The pattern ^Chapter matches "Chapter" only at a line's start; \d+$ matches digits only at a line's end. Combine them — ^\d+$ — and you've described "a line containing nothing but digits", which is the standard trick for validating that a whole input is a number and not just contains one.
The third anchor, \b, is a word boundary: the invisible fence between a word character and a non-word character. \bcat\b matches the word "cat" but not the "cat" inside "concatenate" — the single most useful pattern in this article if you ever search prose.
Groups and alternation: this or that
Parentheses do two jobs. First, they group, so a quantifier applies to more than one character: (ab)+ matches "ab", "abab", "ababab". Second, with a pipe inside, they offer alternatives: (cat|dog|bird) matches any of the three words. You'll use this constantly for small variations — colou?r handles "color" and "colour" with the optional-u trick, and (Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day matches every day of the week.
Groups also capture: most tools remember what each group matched, so you can refer back to it. In a find-and-replace, (\w+)@(\w+) replaced with $2@$1 would flip "user@host" into "host@user". You won't need this daily, but when you do, nothing else substitutes.
The greatest hits: patterns worth memorizing
With the five ideas above you can read — and adapt — the patterns that turn up everywhere. Here are the ones worth committing to memory:
| Job | Pattern | Notes |
|---|---|---|
| Four-digit year | \b\d{4}\b | The boundaries stop "12345" matching |
| Date (YYYY-MM-DD) | \d{4}-\d{2}-\d{2} | Matches format, doesn't validate the date |
| Hex colour | #[0-9a-fA-F]{6}\b | Add ([0-9a-fA-F]{3}){1,2} for 3-digit too |
| Simple email-ish | [\w.+-]+@[\w-]+\.[\w.]+ | Catches typos, not a real validator (see below) |
| URL-ish | https?://\S+ | Good enough for finding links in text |
| Trim whitespace | ^\s+|\s+$ | Replace with nothing; alternation at work |
| Duplicate words | \b(\w+)\s+\1\b | Finds "the the" — a proofreader's friend |
Greedy versus lazy: the one surprise
Quantifiers are greedy by default: they grab as much as they can. On the text <b>hi</b> <i>yo</i>, the pattern <.*> matches the entire string from the first < to the last > — one big match, not two tags. Add a question mark — <.*?> — and the quantifier turns lazy, matching as little as possible: now you get <b>, then </b>, and so on. Whenever a pattern matches "too much", laziness is the first thing to try.
Where regex stops being the answer
Honesty requires the caveats. First, regex can't count nesting — it cannot reliably match balanced parentheses or parse HTML, because those need memory of depth. The famous advice stands: to parse HTML, use an HTML parser, not a pattern. Second, some patterns are explosively slow: (a+)+$ tested against twenty a's followed by a "b" can take effectively forever, a trap called catastrophic backtracking. If a pattern hangs, simplify the nesting. Third, "validating" an email address with regex is a rite of passage that ends in tears — the real specification is hundreds of pages long, and every strict pattern rejects some valid address. For forms, check for an @ and a dot, then send a confirmation email; that's the only validation that ever worked.
But within its lane — finding, extracting and tidying text — nothing beats it. Learn the five ideas above and you'll recognize 80% of the patterns you'll ever meet, and write most of the ones you'll ever need.