Core Syntax
Everything in a regex is either a literal (matches itself) or a metacharacter (means something). This page covers the full set of building blocks, and flags where each engine disagrees.
Literals and metacharacters
Section titled “Literals and metacharacters”Most characters match themselves. cat matches the three letters c, a, t in sequence — anywhere in the input, not just at the start.
printf 'concatenate\ndog\n' | grep -E 'cat'# => concatenateThese characters are special almost everywhere:
. ^ $ * + ? ( ) [ ] { } | \ // is only special in JavaScript regex literals (it terminates the pattern). In POSIX BRE, + ? { } ( ) | are not special unless backslashed — the inverse rule, covered in flags and engines.
Escaping
Section titled “Escaping”Backslash a metacharacter to make it literal.
/3.14/.test('3x14') // => true — "." matches any character/3\.14/.test('3x14') // => false — now it's a real dot/3\.14/.test('3.14') // => trueEscaping a non-metacharacter is either a shorthand class (\d, \w, \b) or an error. Do not sprinkle backslashes defensively.
import rere.compile(r"\p{L}")# => re.PatternError: bad escape \p at position 0 (Python's re has no \p)When the “pattern” comes from user input or a variable, escape it programmatically. Never build a regex by string concatenation without escaping.
// ES2025 — recent Node and ChromeRegExp.escape('a.b*c') // => \x61\.b\*c (over-escapes on purpose; still matches "a.b*c")
// Portable fallbackconst esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');new RegExp('^' + esc(userInput) + '$');import rere.escape("a.b*c") # => a\.b\*cre.compile("^" + re.escape(user_input) + "$")Character classes
Section titled “Character classes”[...] matches exactly one character from the set inside.
/[aeiou]/.test('rhythm') // => false'grey'.replace(/gr[ea]y/, 'X') // => "X"Ranges
Section titled “Ranges”A hyphen between two characters means “everything in between”, by code point.
[a-z] lowercase ASCII letters[A-Za-z] ASCII letters, both cases[0-9] ASCII digits[a-zA-Z0-9_] the classic "word character" setprintf 'Hello World\nfoo_bar 42\n' | grep -oE '[A-Z][a-z]+'# => Hello# => WorldNegation
Section titled “Negation”A ^ as the first character inside the brackets inverts the set.
/[^0-9]/.test('12345') // => false — every char is a digit'a1b2'.replace(/[^0-9]/g, '') // => "12"Elsewhere in the class, ^ is a literal caret: [a^b] matches a, ^, or b.
Characters that need escaping inside [ ]
Section titled “Characters that need escaping inside [ ]”Inside a class, almost everything loses its special meaning. Only four things matter:
| Character | Rule |
|---|---|
] |
Escape it: [\]]. (Or place it first: []] — POSIX-only trick.) |
\ |
Always escape: [\\] |
^ |
Only special as the first character |
- |
Only special between two characters; put it first or last: [-a-z], [a-z-] |
/^[-+*/]$/.test('*') // => true — "-" first is literal, "." and "*" are inert here/[.]/.test('a.b') // => true — a dot in a class is just a dotPOSIX character classes
Section titled “POSIX character classes”Inside brackets, [:name:] names a locale-aware set. These work in POSIX ERE/BRE (grep, sed, awk), PCRE, and Ruby — but not in JavaScript or Python re.
| Class | Equivalent |
|---|---|
[:alpha:] |
letters |
[:digit:] |
0-9 |
[:alnum:] |
letters + digits |
[:space:] |
space, tab, newline, CR, FF, VT |
[:upper:] / [:lower:] |
case |
[:punct:] |
punctuation |
[:xdigit:] |
hex digits |
[:blank:] |
space and tab only |
Note the double brackets — the outer pair is the character class, the inner is the POSIX name:
printf 'Hello World\n' | grep -oE '[[:upper:]][[:alpha:]]+'# => Hello# => World
printf 'a\tb\n' | grep -cE '[[:space:]]'# => 1/[[:digit:]]/.test('5') // => false in JS! It's a class of "[", ":", "d", "i", "g"...Union and subtraction
Section titled “Union and subtraction”Plain regex has no set subtraction. JavaScript’s v flag (ES2024) adds it:
// v flag: [A--[aeiou]] style subtraction and intersection/^[\p{L}--[\p{Lu}]]+$/v.test('abc') // letters, minus uppercase => true/^[\p{L}--[\p{Lu}]]+$/v.test('Abc') // => falseElsewhere, express subtraction with a negative lookahead: (?![aeiou])[a-z].
The dot
Section titled “The dot”. matches any single character except a newline, in every engine, by default.
/a.c/.test('abc') // => true/a.c/.test('a\nc') // => false/a.c/s.test('a\nc') // => true — "s" (dotAll) flagimport rebool(re.search(r"a.b", "a\nb")) # => Falsebool(re.search(r"a.b", "a\nb", re.S)) # => True (re.S == re.DOTALL)Predefined shorthand classes
Section titled “Predefined shorthand classes”Perl-derived engines (JS, Python, PCRE, .NET, Java, Go) provide these. POSIX ERE does not — GNU tools support \w, \s, \b as extensions, but \d is not portable.
| Shorthand | Meaning | Negation |
|---|---|---|
\d |
digit | \D |
\w |
word character: letters, digits, _ |
\W |
\s |
whitespace: space, tab, newline, CR, FF, VT | \S |
They work inside character classes too: [\w.-] is “word char, dot, or hyphen”.
'phone: 555-0100'.match(/\d{3}-\d{4}/) // => ["555-0100"]'a b\tc'.split(/\s+/) // => ["a", "b", "c"]Their meaning is not the same everywhere
Section titled “Their meaning is not the same everywhere”This is a real portability trap:
// JavaScript: \d and \w are ASCII-only, even with the u flag/\d/u.test('٣') // => false (Arabic-Indic digit three)/\p{Nd}/u.test('٣') // => trueimport rere.findall(r"\d", "٣ 3") # => ['٣', '3'] — Unicode-aware by defaultre.findall(r"\d", "٣ 3", re.ASCII) # => ['3']In Python, \w and \s are likewise Unicode-aware for str patterns (ASCII-only for bytes). In PCRE2, behavior depends on whether PCRE2_UCP is set. Do not assume.
For POSIX tools, use [0-9] or [[:digit:]] instead of \d:
grep -E '[0-9]{2,}' file.txt # portablegrep -E '\d{2,}' file.txt # NOT portable — fails on BSD/macOS grepAnchors
Section titled “Anchors”Anchors match a position, not a character. They consume nothing.
^ and $
Section titled “^ and $”^ is the start of the input; $ is the end. With the multiline flag, they also match at every line boundary.
/^abc$/.test('abc') // => true/^abc$/.test('xabcx') // => false
'a\nb'.match(/^b$/) // => null'a\nb'.match(/^b$/m) // => ["b"] — m makes ^/$ line anchorsimport rere.findall(r"^b$", "a\nb") # => []re.findall(r"^b$", "a\nb", re.M) # => ['b']In grep, every line is its own subject, so ^ and $ are line anchors by default:
printf 'cat\nconcat\n' | grep -E '^cat$'# => cat\b and \B — word boundaries
Section titled “\b and \B — word boundaries”\b matches where a \w character sits next to a non-\w character (or a string edge). \B matches everywhere \b does not.
'cat catalog'.replace(/\bcat\b/g, 'X') // => "X catalog"'cat catalog'.replace(/cat/g, 'X') // => "X Xalog" — no boundary/\Bcat/.test('concat') // => true — "cat" not at a word startprintf 'cat\ncatalog\n' | grep -E '\bcat\b' # GNU extension# => catawk is the odd one out: \b means backspace there. Use gawk’s \y, or \< and \>:
printf 'cat catalog\n' | awk '{gsub(/\ycat\y/, "X"); print}'# => X catalogprintf 'cat catalog\n' | awk '{print match($0, /\<cat\>/)}'# => 1\b inside a character class is different again: [\b] means a literal backspace in JS and Python. Never put \b in brackets.
Other position assertions: \A (absolute start) and \Z / \z (absolute end) exist in Python and PCRE but not in JavaScript.
Quantifiers
Section titled “Quantifiers”Quantifiers repeat the immediately preceding element — one character, one class, or one group.
| Quantifier | Repeats |
|---|---|
* |
0 or more |
+ |
1 or more |
? |
0 or 1 (optional) |
{n} |
exactly n |
{n,} |
n or more |
{n,m} |
between n and m |
/colou?r/.test('color') // => true/colou?r/.test('colour') // => true/^\d{4}$/.test('2024') // => true/^\d{2,4}$/.test('12345') // => false{,m} (no lower bound) is not standard — in JS and Python it is a literal {,m}. Write {0,m}.
Quantifiers apply to the last token, so wrap multi-character sequences:
/ab+/.test('abbb') // => true — "+" applies to "b" only/(ab)+/.test('abab') // => true — "+" applies to the groupIn POSIX BRE, +, ?, and {n,m} all need backslashes:
printf 'aaa\n' | grep -c 'a\{2,\}' # => 1printf 'aaa\n' | grep -c 'a{2,}' # => 0 (BRE: literal braces)printf 'aaa\n' | grep -cE 'a{2,}' # => 1 (ERE)Greedy vs lazy
Section titled “Greedy vs lazy”By default, quantifiers are greedy: they consume as much as possible, then give characters back one at a time until the rest of the pattern can match.
'<a><b>'.match(/<.+>/)[0] // => "<a><b>" greedy — grabs everything, backs off to the last ">"'<a><b>'.match(/<.+?>/)[0] // => "<a>" lazy — takes the minimumAppending ? to any quantifier makes it lazy (also called non-greedy or reluctant): *?, +?, ??, {n,m}?.
import rere.match(r'"(.*)"', '"a" and "b"').group(1) # => 'a" and "b're.match(r'"(.*?)"', '"a" and "b"').group(1) # => 'a'Possessive quantifiers and atomic groups
Section titled “Possessive quantifiers and atomic groups”*+, ++, ?+, {n,m}+ match greedily and refuse to give anything back. (?>...) (atomic group) does the same for a whole subpattern. Both exist to kill backtracking.
- PCRE / Java / Ruby: both supported.
- Python: supported since 3.11.
- JavaScript: neither is supported. Emulate atomicity with a lookahead plus a backreference:
(?=(a+))\1.
import rere.compile(r"^(?>a+)+$").match("a" * 24 + "b") # returns instantlyre.compile(r"^(a+)+$").match("a" * 24 + "b") # ~1 second, and doubles per extra "a"Alternation
Section titled “Alternation”| means “either side”. It has the lowest precedence of all regex operators, so it splits the entire enclosing group — or the entire pattern if there is no group.
^cat|dog$ does not mean “the whole string is cat or dog”. It means (^cat) or (dog$):
/^cat|dog$/.test('catx') // => true — "^cat" matches, the "$" is on the other branch/^cat|dog$/.test('xxdog') // => true — "dog$" matches/^(cat|dog)$/.test('catx') // => false — anchors now apply to both alternatives/^(cat|dog)$/.test('dog') // => trueAlternatives are tried left to right, and the first one that lets the overall match succeed wins — even if a later one would match more text.
'catalog'.match(/cat|catalog/)[0] // => "cat"'catalog'.match(/catalog|cat/)[0] // => "catalog"Order longest-first when the alternatives share a prefix.
Cross-engine:
grep -E 'cat|dog' file.txt # EREgrep 'cat\|dog' file.txt # BRE (GNU)sed -E 's/(cat|dog)/pet/' file.txtGrouping
Section titled “Grouping”Parentheses do two jobs at once: they group for precedence, and they capture the matched text for later use.
/^(ab)+$/.test('ababab') // => true — grouping'2024-01'.match(/(\d{4})-(\d{2})/).slice(1) // => ["2024", "01"] — capturingGroups are numbered by the position of their opening parenthesis, left to right, starting at 1. Group 0 is the whole match.
import rem = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", "2024-01-31")m.group(0) # => '2024-01-31'm.groups() # => ('2024', '01', '31')When you only need grouping, use a non-capturing group (?:...) — it keeps group numbers clean and is marginally cheaper:
'2024-01-31'.match(/^(\d{4})(?:-\d{2}){2}$/) // => ["2024-01-31", "2024"]Capturing, backreferences, named groups, and lookaround are covered in depth on groups and lookaround.
In BRE, groups need backslashes — and so do the backreferences that use them:
echo 'John Smith' | sed 's/\(\w\+\) \(\w\+\)/\2, \1/' # BREecho 'John Smith' | sed -E 's/(\w+) (\w+)/\2, \1/' # ERE# both => Smith, JohnPrecedence, summarised
Section titled “Precedence, summarised”From tightest to loosest binding:
- Escaped characters and character classes —
\d,[a-z] - Grouping —
(...),(?:...) - Quantifiers —
*,+,?,{n,m}(bind to the single preceding token) - Concatenation —
abmeans “a then b” - Alternation —
|
So ab|cd is (ab)|(cd), and ab* is a(b*).
Key points
Section titled “Key points”- Escape metacharacters to match them literally; escape user input programmatically with
RegExp.escape/re.escape. [ ]matches one character;^inside negates it;-is only a range when it sits between two characters.- POSIX classes
[[:digit:]]work in grep/sed/awk/PCRE but not in JS or Python. \d\w\sare Perl-family only, and their Unicode behavior differs: ASCII in JS, Unicode in Python..never matches a newline without the dotall flag; negated classes always do.- Prefer bounded classes (
[^"]*) over.*and.*?. - Anchors match positions, not characters;
\bmeans backspace in awk (use\yor\<\>). - Alternation binds loosest — group it.