TL;DR
Match in three steps: anchor the position (^ $ \b), choose the character class (\d \w [0-9]), then control quantity (+ {n,m}). Top pitfalls: grep -E has no \d (POSIX ERE — use [0-9] or grep -P); JS lookbehind (?<=...) is only fully cross-platform since Safari 16.4 (March 2023); greedy matching swallows a whole line — use lazy ?.
Basic Syntax
| Syntax | Meaning | Example | ||
|---|---|---|---|---|
. | Any character (usually not newline) | a.c matches abc, a1c | ||
\d / \w / \s | Digit / word char / whitespace | see engine differences below | ||
[abc] / [^abc] | Class / negated class | [0-9]{3} = three digits | ||
^ / $ | Start / end of line | ^https only matches line start | ||
\b | Word boundary | \bcat\b does not match category | ||
* / + / ? | 0+ / 1+ / 0 or 1 | colou?r matches color/colour | ||
*? / +? | Lazy (non-greedy) quantifiers | .*? matches as little as possible | ||
{n,m} / {n,} / {n} | n to m / at least n / exactly n | \d{4}-\d{2}-\d{2} | ||
(...) / (?:...) | Capturing / non-capturing group | capturing groups support backrefs | ||
(?<name>...) | Named group | Python/PCRE/JS | ||
| `a | b` | Alternation | `(cat | dog)s?` |
(?=...) / (?!...) | Positive / negative lookahead | \d(?=px) matches the 2 in 2px | ||
(?<=...) / (?<!...) | Positive / negative lookbehind | see engine differences |
Copy-Paste Validation Patterns
| Target | Pattern | Note | ||||||
|---|---|---|---|---|---|---|---|---|
| Email (simplified) | [\w.+-]+@[\w-]+\.[\w.-]+ | format check only; use a library in production | ||||||
| URL | https?://[\w.-]+(/[\w./?%&=+-]*)? | includes path & query | ||||||
| Date yyyy-mm-dd | \d{4}-\d{2}-\d{2} | format check; semantic validation is separate | ||||||
| IPv4 (strict 0-255) | `((25[0-5] | 2[0-4]\d | 1\d\d | [1-9]?\d)\.){3}(25[0-5] | 2[0-4]\d | 1\d\d | [1-9]?\d)` | the simple (\d{1,3}\.){3}\d{1,3} accepts 999 |
| China mobile phone | 1[3-9]\d{9} | 11 digits, 2nd digit 3-9 | ||||||
| Chinese characters | [\u4e00-\u9fa5] | CJK Unified Ideographs | ||||||
| Blank line | ^\s*$ | bulk cleanup | ||||||
| Hex color | #[0-9a-fA-F]{6}\b | use {3} for short form | ||||||
| 24h time HH:MM | `([01]\d | 2[0-3]):[0-5]\d` | 23:59 ok, 24:00 not |
Engine Differences (where people get it wrong)
| Feature | PCRE (grep -P / PHP / most languages) | ERE (grep -E / sed -E) | JavaScript |
|---|---|---|---|
\d \w \s shorthand | Yes | No — use [0-9] etc. | Yes |
| Named groups | (?<name>) | No | (?<name>) |
Lookbehind (?<=) | Yes | No | Safari 16.4+ (cross-platform 2023-03) |
Lazy quantifier *? | Yes | GNU yes (not POSIX) | Yes |
| Replacement refs | $1 or \1 | \1 | $1 |
Command-Line & Code Examples
# Count occurrences of each IPv4 in access.log (grep -E: use [0-9], not \d)
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort | uniq -c | sort -rn | head
# Count 5xx errors
grep -cE 'HTTP/[0-9.]+" [5][0-9]{2}' app.log
# sed: convert 2026-08-07 to 2026/08/07 (ERE; group refs use \1)
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\1\/\2\/\3/g' data.txt
# Keep non-empty lines
grep -E '.+' file.txt
# Python re: extract all Chinese characters
import re
re.findall(r'[\u4e00-\u9fa5]+', 'hello 世界') # ['世界']
# Greedy vs lazy when a line has two <a> tags
re.findall(r'<a>(.*?)</a>', '<a>1</a><a>2</a>') # ['1','2'] (lazy .*?)
re.findall(r'<a>(.*)</a>', '<a>1</a><a>2</a>') # ['1</a><a>2'] (greedy .*)
# Named groups
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2026-08-07')
m.group('year') # '2026'
// JS lookbehind to extract a price (Safari 16.4+)
"价格 299 元".match(/(?<=价格 )\d+/); // ['299']
// Compatible with older Safari: capture group instead
"价格 299 元".match(/价格 (\d+)/)[1]; // '299'
Top Pitfalls
\ddoes not work ingrep -E: POSIX ERE has no\d— use[0-9], or switch togrep -P(PCRE).- Greedy matching swallows the line:
.matches as much as possible, so<a>(.)</a>runs to the last</a>; use.*?. - Cannot match Chinese:
\wdoes not include Chinese; use[\u4e00-\u9fa5](Python/JS) or\p{Script=Han}in PCRE with UCP enabled. - Shell escaping: inside double quotes
$is expanded by the shell — write\$; wrapping the whole regex in single quotes is safest. - JS lookbehind errors on old Safari: lookbehind requires Safari 16.4+; rewrite with a capture group (see above).
FAQ
Why doesn't \d match digits in grep?
grep -E (POSIX ERE) has no \d shorthand — use [0-9]. GNU grep only supports \d with grep -P (PCRE). Example: grep -Eo '[0-9]{4}', not grep -Eo '\d{4}'.
How do I extract text between two tags?
Use the lazy .?: <a>(.?)</a>. A greedy .* matches up to the last closing tag and swallows everything in between — especially visible when a line contains multiple tags.
How do I match Chinese characters with regex?
Use [\u4e00-\u9fa5] (CJK Unified Ideographs; supported directly in Python/JS); in PCRE enable UCP and use \p{Script=Han}. Note \w only covers ASCII word characters and does not match Chinese.
Is JS lookbehind supported in all browsers?
Lookbehind (?<=...) has been supported since ES2018, fully cross-platform since Safari 16.4 (about March 2023). For older Safari, rewrite with a capture group: /价格 (\d+)/ instead of /(?<=价格 )\d+/.
Is a regex enough to validate email addresses?
No. Regex only does a loose format check ([\w.+-]+@[\w-]+\.[\w.-]+); it cannot verify that the domain exists or has valid MX records. Use a dedicated library in production (Python email-validator, JS validator.js).
Sources
- MDN Regular Expressions (lookbehind compatibility; cross-platform since 2023-03)
- regular-expressions.info
- GNU grep manual (ERE vs PCRE)
- caniuse.com/js-regexp-lookbehind (Safari 16.4+)
- Verified: 2026-08-07