DS DevShelfHub Projects · AI tools
Cheatsheets / Regex
Cheatsheet · Dev tooling

Regex: Character Classes, Groups and Lookarounds Reference Guide

By DevShelfHub

Character classes, anchors, quantifiers, groups, lookarounds — with examples that actually parse the things you need.

65 items 5 min Patterns Groups Lookarounds

Start hereQuick start · 6 you’ll reach for daily

Word\bword\b
Digits\d+
Whitespace\s+
Any char.
Either / orfoo|bar
Named group(?P<name>…)

flavors coveredVersions

Flavors: PCRE / Python re JavaScript POSIX ERE

Most syntax in this sheet works in PCRE, Python re, and modern JavaScript (ES2018+). Where they differ — named groups, lookbehind, possessive quantifiers — the row flags it. POSIX BRE (basic grep, default sed) is older; use grep -E or sed -E to get ERE.

positions, not charactersAnchors

^Start of string (or line with m flag).
$End of string (or line with m flag).
\AStart of string, always. Ignores m.
\ZEnd of string, always. Ignores m.
\bWord boundary. Between \w and non-\w.
\BNon-boundary. Inside a word.
Anchors match a position, not a character. They consume zero width — useful inside lookarounds and at split points.

match a setCharacter classes

.Any char except newline. s flag makes it match \n too.
[abc]Any of a, b, c.
[^abc]Anything except those.
[a-z]Range. Mix: [A-Za-z0-9_].
\d / \DDigit / non-digit.
\w / \WWord char ([A-Za-z0-9_]) / non.
\s / \SWhitespace / non.
\p{L} / \p{N} / \p{Sc}Unicode property: Letter / Number / Currency. PCRE + JS with u + Python regex module.
[[:alpha:]] / [[:digit:]]POSIX classes. Work inside [].
\t \n \r \f \vTab / LF / CR / form-feed / vertical tab.

how manyQuantifiers

?0 or 1.
*0 or more.
+1 or more.
{n}Exactly N.
{n,}N or more.
{n,m}Between N and M, inclusive.
*? +? ?? {n,m}?Preferred Lazy / non-greedy. Match as little as possible.
*+ ++ ?+ {n,m}+Possessive. No backtracking. PCRE / Java. Massive perf win on big inputs.
Default quantifiers are greedy. <.*> on <a><b> matches the whole thing. Use lazy <.*?> when you want the smallest span.

capture & referenceGroups

(abc)Capturing group. Numbered \1 from left.
(?:abc)Non-capturing group. Use for grouping without polluting captures.
(?P<name>abc)Preferred Named capture (Python).
(?<name>abc)Named capture (PCRE, JS, .NET).
(?P=name) / \k<name>Backreference to a named group.
(?i:abc) / (?m:abc) / (?s:abc)Inline flags scoped to a group.
(?>abc)Atomic group. Like possessive: refuses to backtrack.
\1 \2 \3 …Backreference in the pattern (e.g. detect duplicate words: \b(\w+) \1\b).
$1 $2 (or \1 \2)Reference in the replacement. Syntax depends on engine.

match position, not textLookarounds

(?=abc)Positive lookahead. Followed by abc.
(?!abc)Negative lookahead.
(?<=abc)Positive lookbehind. Preceded by abc.
(?<!abc)Negative lookbehind.
\d+(?=px)Numbers followed by px. px isn’t consumed.
(?<=\$)\d+Digits after a literal $.
JavaScript shipped variable-width lookbehind in ES2018; Python’s re still requires fixed width. The third-party regex module on Python lifts that limit.

refer to a matchBackreferences

(\w+)\s+\1Duplicate word: the the.
^(.+)\n\1$Two identical lines (with s or two lines worth of context).
<(\w+)>.*?</\1>Naive XML pair. Don’t parse XML with regex in earnest; for grep, fine.
(?P=name)Backreference by name (Python).
$1 / \1 in replacementJS uses $1; sed / Python use \1.

modify the engineFlags

iCase-insensitive.
mMultiline. ^ and $ match line boundaries.
sDotall. . matches newlines too.
xVerbose / extended. Whitespace ignored, # starts a comment. Python: re.VERBOSE. PCRE: /x.
gGlobal. JS only — iterates all matches. Python findall is always global.
uUnicode-aware. JS: enable \p{} + correct . width.
ySticky (JS). Anchors at lastIndex.
(?i) / (?m) / (?s)Inline flag for the rest of the pattern.

flavor differencesEngines & usage

Python

python
import re

# Find / match
re.search(r"\bfoo\b", text)           # first match, anywhere
re.match (r"foo",      text)          # only at the start
re.fullmatch(r"\d{4}", "2026")        # the whole string must match
re.findall(r"\d+",     text)          # list of all non-overlapping
list(re.finditer(r"\d+", text))       # list of Match objects (with .span())

# Replace
re.sub(r"\s+",  " ",   text)          # collapse whitespace
re.sub(r"(\w+)@(\w+)", r"\1 at \2", t)  # backrefs in replacement

# Groups
m = re.search(r"(?P\d{3})-(?P\d{4})", "415-1234")
m.group("area"), m["num"], m.groupdict()

# Compile + flags for hot loops
pat = re.compile(r"^error:", re.MULTILINE | re.IGNORECASE)
for line in pat.finditer(log):
    ...

JavaScript

javascript
// Test / match
/foo/.test("foobar")                   // boolean
"foobar".match(/o+/g)                  // ["oo"]  (all matches with /g)
"foobar".search(/b/)                   // index of first match

// Iterate with capture groups
const re = /(\d{4})-(\d{2})-(\d{2})/g;
for (const m of "2026-05-17 and 2026-01-02".matchAll(re)) {
  const [, y, mo, d] = m;
  console.log(y, mo, d);
}

// Named groups (ES2018+)
const pat = /(?\d{3})-(?\d{4})/;
const { groups } = "415-1234".match(pat);
groups.area;                            // "415"

// Replace with a function
"x = 1 + 2".replaceAll(/\d+/g, n => String(Number(n) * 10));

// Common flags
//   g  global       i  ignore case     m  multiline (^/$)
//   s  dotAll       u  unicode         y  sticky

copy-paste recipesCommon patterns

bash
# Email (good-enough, not RFC-perfect)
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}

# IPv4 (with bounds — each octet 0–255)
\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b

# URL (http/https, no creds, optional path/query)
https?://[^\s/$.?#].[^\s]*

# ISO 8601 date (yyyy-mm-dd)
\b\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b

# UUID v4
\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b

# Semver
\bv?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.\-]+)?(?:\+[\w.\-]+)?\b

# Markdown link  →  [text](url)
\[([^\]]+)\]\(([^)]+)\)

# Trim trailing whitespace on every line
[ \t]+$    →  ""    (multiline)
^\s*$Empty / whitespace-only line.
^\s+|\s+$Leading / trailing whitespace. Replace with empty.
\b\w+\bTokenise into words. ASCII; use \p{L}+ for Unicode.
#[a-fA-F0-9]{3,8}\bHex colour, 3 / 4 / 6 / 8 digits.
^([A-Z]+):\s+Lines like ERROR: , TODO: .
(?P<ts>\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})ISO timestamp capture.
(?P<k>\w+)="(?P<v>[^"]*)"Quoted key=value pairs.

parse a log fileEnd-to-end · Log parser

Verbose-mode pattern with named groups + an optional final field. Counts errors per service.

python
import re
from pathlib import Path

# Parse "2026-05-17 12:34:56 ERROR [orders] failed: ConnectTimeout"
LINE = re.compile(
    r"""
    ^(?P\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})  # timestamp
    \s+(?PDEBUG|INFO|WARN|ERROR)             # level
    \s+\[(?P[\w.\-]+)\]                        # service
    \s+(?P.+?)                                 # message (lazy)
    (?::\s+(?P\w+))?$                          # optional exception
    """,
    re.VERBOSE,
)

errors_by_service: dict[str, int] = {}

for line in Path("app.log").read_text().splitlines():
    m = LINE.match(line)
    if not m or m["level"] != "ERROR":
        continue
    errors_by_service[m["svc"]] = errors_by_service.get(m["svc"], 0) + 1

for svc, n in sorted(errors_by_service.items(), key=lambda kv: -kv[1]):
    print(f"{n:5d}  {svc}")

Best practiceGood to know

Verbose mode is free. re.VERBOSE / /x let you split a pattern over lines with comments. Once a regex needs a docstring to explain, switch.
Prefer named groups in anything you’ll re-read. (?P<ts>…) beats m.group(1). Reordering groups later doesn’t break the call site.
Compile once when you re-use a pattern. pat = re.compile(…) outside the loop is measurably faster than module-level re.search for hot paths.

Common trapsWatch out for

Catastrophic backtracking. Nested unbounded quantifiers on overlapping classes (e.g. (a+)+$) explode on near-miss inputs. Anchor, use possessive / atomic groups, or rewrite to a deterministic shape.
. excludes newlines by default. A “match everything” pattern across lines needs the s / DOTALL flag — or [\s\S] as a portable workaround.
Don’t parse HTML / JSON / email addresses with regex for real work. Use a parser. Regex is fine to find things in those formats; building a validator with it leads to bugs that hide for months.

Go deeperSee also

Regex FAQ

What is regex used for?

Regular expressions match and extract text patterns. Common uses include input validation (email, phone number, postal code), log parsing, find-and-replace in editors, URL routing, tokenizing data, and scraping structured fields from unstructured text.

What is the difference between greedy and lazy quantifiers in regex?

Greedy quantifiers (+, *, {n,}) match as many characters as possible. Lazy quantifiers (+?, *?, {n,}?) match as few as possible, stopping at the first opportunity. Use lazy matching when you need the shortest possible match, such as extracting content between two HTML tags.

What are lookaheads and lookbehinds in regex?

Lookaheads (?=...) and lookbehinds (?<=...) assert that a pattern exists ahead of or behind the current position without consuming characters. Negative variants (?!...) and (?<!...) assert absence. They are useful for matching a word only when followed or preceded by a specific context.

What is the difference between capturing and non-capturing groups?

A capturing group (...) saves the matched text and makes it available as a back-reference or in the match result. A non-capturing group (?:...) groups tokens for alternation or quantification without saving the match. Use non-capturing groups when you do not need to reference the matched text.

Do regex patterns work the same in all languages?

No. Engines differ in what they support: JavaScript lacks lookbehinds in older environments, Python re uses different escape rules than re2-style engines, and PCRE supports atomic groups and possessive quantifiers that most other engines do not. Always check engine-specific docs.