Regular expressions have a reputation for being unreadable, and honestly some of them are. But the everyday useful subset is small. Learn about eight concepts and you can handle most real search-and-validate work.
The building blocks
- \d matches any digit, \w any letter/digit/underscore, \s any whitespace. Capitalise them (\D, \W, \S) to invert the match.
- . matches any single character. It is the most over-used and most misused symbol in regex.
- [abc] matches any one of a, b, or c. [a-z] matches any lowercase letter. [^abc] matches anything except those.
- * means zero or more, + means one or more, ? means zero or one.
- {3} means exactly three, {2,4} means two to four, {2,} means two or more.
- ^ anchors to the start of the string, $ to the end. Without them you match anywhere inside.
- ( ) groups things together and captures what matched, so you can extract it afterwards.
- | means or — cat|dog matches either word.
Escaping: the first thing that bites people
Characters like . * + ? ( ) [ ] { } | ^ $ and \ all have special meaning. To match one literally, put a backslash before it. Searching for a price with the pattern $5.00 will not do what you expect — $ means end-of-string and . means any character. You want \$5\.00.
Greedy versus lazy
By default, * and + are greedy: they grab as much text as possible. Run /<.*>/ against "<b>bold</b>" and it matches the entire string, not just the first tag, because .* happily swallows everything up to the last >.
Adding a ? makes them lazy — /<.*?>/ stops at the first >, matching just "<b>". This single character fixes a large share of "why is my regex matching too much" problems.
Flags that matter
The global flag (g) is the one people forget. Without it, methods like match() return only the first result, which looks like a broken pattern when it's really a missing flag. The case-insensitive flag (i) is self-explanatory. The multiline flag (m) makes ^ and $ match at each line break rather than only at the start and end of the whole string.
A warning about catastrophic backtracking
Nested quantifiers like (a+)+ can make matching time grow exponentially with input length. On short strings it's instant; on a long one it can hang the page entirely. If a pattern is fast on test data but freezes on real input, look for nested repetition — usually the outer group can be flattened away.
Flavours differ
Core syntax is shared, but the details are not. JavaScript, Python's re module, PCRE, and .NET differ on named capture groups, lookbehind support, and Unicode handling. Test a pattern in the language you'll actually run it in — a regex validated in one flavour is not guaranteed elsewhere.