Skip to content

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.

Most characters match themselves. cat matches the three letters c, a, t in sequence — anywhere in the input, not just at the start.

Terminal window
printf 'concatenate\ndog\n' | grep -E 'cat'
# => concatenate

These 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.

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') // => true

Escaping a non-metacharacter is either a shorthand class (\d, \w, \b) or an error. Do not sprinkle backslashes defensively.

import re
re.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 Chrome
RegExp.escape('a.b*c') // => \x61\.b\*c (over-escapes on purpose; still matches "a.b*c")
// Portable fallback
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
new RegExp('^' + esc(userInput) + '$');
import re
re.escape("a.b*c") # => a\.b\*c
re.compile("^" + re.escape(user_input) + "$")

[...] matches exactly one character from the set inside.

/[aeiou]/.test('rhythm') // => false
'grey'.replace(/gr[ea]y/, 'X') // => "X"

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" set
Terminal window
printf 'Hello World\nfoo_bar 42\n' | grep -oE '[A-Z][a-z]+'
# => Hello
# => World

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.

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 dot

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:

Terminal window
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"...

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') // => false

Elsewhere, express subtraction with a negative lookahead: (?![aeiou])[a-z].

. 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) flag
import re
bool(re.search(r"a.b", "a\nb")) # => False
bool(re.search(r"a.b", "a\nb", re.S)) # => True (re.S == re.DOTALL)

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"]

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('٣') // => true
import re
re.findall(r"\d", "٣ 3") # => ['٣', '3'] — Unicode-aware by default
re.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:

Terminal window
grep -E '[0-9]{2,}' file.txt # portable
grep -E '\d{2,}' file.txt # NOT portable — fails on BSD/macOS grep

Anchors match a position, not a character. They consume nothing.

^ 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 anchors
import re
re.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:

Terminal window
printf 'cat\nconcat\n' | grep -E '^cat$'
# => cat

\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 start
Terminal window
printf 'cat\ncatalog\n' | grep -E '\bcat\b' # GNU extension
# => cat

awk is the odd one out: \b means backspace there. Use gawk’s \y, or \< and \>:

Terminal window
printf 'cat catalog\n' | awk '{gsub(/\ycat\y/, "X"); print}'
# => X catalog
printf '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 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 group

In POSIX BRE, +, ?, and {n,m} all need backslashes:

Terminal window
printf 'aaa\n' | grep -c 'a\{2,\}' # => 1
printf 'aaa\n' | grep -c 'a{2,}' # => 0 (BRE: literal braces)
printf 'aaa\n' | grep -cE 'a{2,}' # => 1 (ERE)

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 minimum

Appending ? to any quantifier makes it lazy (also called non-greedy or reluctant): *?, +?, ??, {n,m}?.

import re
re.match(r'"(.*)"', '"a" and "b"').group(1) # => 'a" and "b'
re.match(r'"(.*?)"', '"a" and "b"').group(1) # => 'a'

*+, ++, ?+, {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 re
re.compile(r"^(?>a+)+$").match("a" * 24 + "b") # returns instantly
re.compile(r"^(a+)+$").match("a" * 24 + "b") # ~1 second, and doubles per extra "a"

| 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') // => true

Alternatives 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:

Terminal window
grep -E 'cat|dog' file.txt # ERE
grep 'cat\|dog' file.txt # BRE (GNU)
sed -E 's/(cat|dog)/pet/' file.txt

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"] — capturing

Groups are numbered by the position of their opening parenthesis, left to right, starting at 1. Group 0 is the whole match.

import re
m = 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:

Terminal window
echo 'John Smith' | sed 's/\(\w\+\) \(\w\+\)/\2, \1/' # BRE
echo 'John Smith' | sed -E 's/(\w+) (\w+)/\2, \1/' # ERE
# both => Smith, John

From tightest to loosest binding:

  1. Escaped characters and character classes — \d, [a-z]
  2. Grouping — (...), (?:...)
  3. Quantifiers — *, +, ?, {n,m} (bind to the single preceding token)
  4. Concatenation — ab means “a then b”
  5. Alternation — |

So ab|cd is (ab)|(cd), and ab* is a(b*).

  • 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 \s are 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; \b means backspace in awk (use \y or \< \>).
  • Alternation binds loosest — group it.