Web Development

Regular Expressions Tutorial: A Practical Guide to Regex

Learn regular expressions from the basics to advanced patterns. This practical tutorial covers regex syntax, common patterns, and real-world examples for text processing.

By Inventive HQ Team

A regular expression (regex) is a compact pattern language for describing sets of strings, so you can search, validate, extract, and replace text with one expression instead of hand-written parsing code. Every pattern is built from four kinds of pieces: literals that match themselves (cat matches "cat"), character classes that match a set (\d = any digit, [a-z] = any lowercase letter), quantifiers that set how many times (+ = one or more, {2,4} = two to four), and anchors that pin a position (^ = start, $ = end, \b = word boundary). A regex engine reads your pattern left to right and walks it across the input, reporting where โ€” and how โ€” it matches.

That's the definition an AI Overview will hand you. What it can't show you is the part that actually trips people up: how the matching engine moves through your text, why .* swallows far more than you meant, and where one unescaped dot silently matches everything. So below you'll find a live regex tester you can type into, an animated view of the engine walking a real pattern, a greedy-vs-lazy diagram, and a copy-paste library of patterns that already handle the edge cases.

Loading interactive tool...

How a Regex Engine Actually Matches

Reading a pattern top to bottom hides the thing that matters most: matching is a left-to-right scan with backtracking, not a lookup. The animation below shows the pattern (\d{3})-(\d{4}) being applied to the string 555-1234. Watch how the engine locks in the first three digits as capture group 1, consumes the literal -, then captures the last four digits as group 2 before declaring a match.

How a regex engine matches a phone-number pattern The pattern (\d{3})-(\d{4}) scanned across the input 555-1234, highlighting capture group 1, the literal dash, and capture group 2 in sequence before a match. Pattern: (\d{3})-(\d{4}) applied to "555-1234"

PATTERN TOKENS (\d{3}) - (\d{4})

INPUT STRING 5 5 5 - 1 2 3 4

group 1 = "555" group 2 = "1234"

MATCH โœ“

The engine consumes characters left to right; each group locks in as its quantifier is satisfied.

The One Concept That Fixes Most Bugs: Greedy vs Lazy

More regex bugs come from greediness than from any other single cause. By default * and + match as much as possible, then give characters back only if the rest of the pattern would otherwise fail. The diagram below shows the same input, <b>bold</b>, matched by a greedy <.*> (grabs everything) and a lazy <.*?> (stops at the first >).

Greedy versus lazy quantifiers on the same input The pattern less-than dot-star greater-than matches the whole string bold, while the lazy version dot-star-question matches only the first tag.

Greedy <.*> โ€” matches too much <b> bold </b> grabs <b>bold</b>

Lazy <.*?> โ€” matches just enough <b> bold </b> stops at first > = <b>

Rule of thumb: reach for the lazy ? whenever a .* or .+ sits between two delimiters.

Regular expressions (regex) are patterns that describe sets of strings. They're one of the most powerful tools for searching, matching, and manipulating text. This tutorial takes you from basic syntax to practical patterns you can use immediately.

Advertisement

Basic Syntax

Literal Characters

Most characters match themselves. The regex cat matches the string "cat" exactly.

Metacharacters

Special characters have meaning beyond their literal value:

CharacterMeaning
.Any single character
^Start of string
$End of string
*Zero or more of preceding
+One or more of preceding
?Zero or one of preceding
\Escape special character

To match a literal metacharacter, escape it with a backslash: \. matches a period.

Character Classes

Square brackets define a set of characters to match:

[abc]     - matches a, b, or c
[a-z]     - matches any lowercase letter
[A-Z]     - matches any uppercase letter
[0-9]     - matches any digit
[a-zA-Z]  - matches any letter
[^abc]    - matches anything except a, b, or c

Shorthand Classes

Common character classes have shortcuts:

\d  - digit [0-9]
\D  - non-digit [^0-9]
\w  - word character [a-zA-Z0-9_]
\W  - non-word character
\s  - whitespace (space, tab, newline)
\S  - non-whitespace

Quantifiers

Quantifiers specify how many times a pattern should match:

a*      - zero or more a's
a+      - one or more a's
a?      - zero or one a
a{3}    - exactly 3 a's
a{2,4}  - 2 to 4 a's
a{2,}   - 2 or more a's

Greedy vs Lazy

By default, quantifiers are greedy---they match as much as possible. Add ? to make them lazy:

".*"   - greedy: matches "hello" and "world" in "hello" and "world"
".*?"  - lazy: matches "hello" then "world" separately

Groups and Capturing

Parentheses create groups:

(abc)+       - one or more "abc" sequences
(cat|dog)    - matches "cat" or "dog"
(\d{3})-(\d{4})  - captures area code and number separately

Groups are numbered starting at 1. Use \1, \2, etc. to reference captured groups:

(\w+)\s+\1   - matches repeated words like "the the"

Non-Capturing Groups

Use (?:...) when you need grouping without capturing:

(?:https?://)?(www\.)?example\.com

Anchors and Boundaries

^       - start of string
$       - end of string
\b      - word boundary
\B      - non-word boundary

Examples:

^Hello      - string starts with "Hello"
world$      - string ends with "world"
\bcat\b     - "cat" as a whole word (not "category")

Lookahead and Lookbehind

Assert something exists (or doesn't) without including it in the match:

foo(?=bar)   - "foo" followed by "bar" (matches "foo" only)
foo(?!bar)   - "foo" not followed by "bar"
(?<=foo)bar  - "bar" preceded by "foo" (matches "bar" only)
(?<!foo)bar  - "bar" not preceded by "foo"

Practical Examples

Email Validation

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

This matches standard email formats. For production, consider using a library.

Phone Numbers

^\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})$

Matches: (555) 123-4567, 555-123-4567, 555.123.4567, 5551234567

URLs

https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^\s]*)?

Matches HTTP and HTTPS URLs with optional paths.

IP Addresses

\b(?:\d{1,3}\.){3}\d{1,3}\b

Basic IPv4 pattern. For strict validation, check each octet is 0-255.

Passwords (Complexity Check)

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$

Requires lowercase, uppercase, digit, special character, and 8+ length.

Common Mistakes (Symptom โ†’ Cause โ†’ Fix)

When a pattern "doesn't work," it almost always falls into one of these buckets. Match the symptom, apply the fix.

SymptomRoot causeFix
Pattern matches nothing, even valid inputBackslash eaten by the string layer before regex sees itIn most languages use double backslashes ("\\d") or a raw/verbatim string (r"\d" in Python, @"\d" in C#)
Matches way more than intendedGreedy .* / .+ consuming past your delimiterMake it lazy: .*? / .+?, or replace . with a negated class like [^>]*
Matches a substring you didn't want (e.g. "123" in "abc123")No anchors โ€” regex matches anywhereAdd ^ and $ (or \b word boundaries) to pin the position
Dot matches things it shouldn't. means "any character," not a literal periodEscape it: \. โ€” or put it in a class [.]
Whole-word search also hits substringsMissing word boundariesWrap the term: \bcat\b won't match "category"
Character class behaves oddlyMetacharacters are mostly literal inside [...]Remember [.], [*], [+] match those literal characters; only ^, -, ], \ are special in a class
Browser tab / server freezes on some inputsCatastrophic backtracking from nested quantifiers like (a+)+Avoid overlapping quantifiers; anchor the pattern; use atomic groups or possessive quantifiers where supported
Works in one language, fails in anotherFlavor differences (lookbehind, named groups, Unicode)Test in the target engine; don't assume PCRE features exist in JavaScript or POSIX grep

Build-and-Verify Checklist

Run every non-trivial pattern through this before it reaches production:

  • Anchor deliberately โ€” decide whether you want a full-string match (^...$) or a search-anywhere match, then add or omit anchors on purpose.
  • Escape every literal metacharacter โ€” dots, slashes, parentheses, and question marks in your target text must be backslash-escaped.
  • Default to lazy between delimiters โ€” swap .* for .*? (or a negated class) whenever it sits between two markers.
  • Test the empty, huge, and malicious input โ€” an empty string, a very long string, and a near-miss that could trigger backtracking.
  • Confirm capture groups โ€” verify each (...) captures exactly what you reference in \1 or your replacement, and switch to (?:...) where you don't need the capture.
  • Match the target flavor โ€” validate the final pattern in the language or tool that will run it, not just an online tester.

Testing Your Patterns

Use our Regex Tester to:

  • Write and test patterns interactively
  • See matches highlighted in real-time
  • View captured groups
  • Get explanations of your pattern

Testing patterns before using them in code catches errors early and helps you understand exactly what your regex matches.

Quick Reference

.       any character
^       start of string
$       end of string
\d      digit
\w      word character
\s      whitespace
[abc]   character class
[^abc]  negated class
a*      zero or more
a+      one or more
a?      optional
a{n}    exactly n
a{n,m}  n to m times
(...)   capturing group
(?:...) non-capturing group
a|b     alternation
\b      word boundary
(?=...) positive lookahead
(?!...) negative lookahead

Regular expressions are powerful but can be complex. Start simple, test thoroughly, and build up complexity as needed.

Frequently Asked Questions

What is a regular expression in simple terms?

A regular expression (regex) is a compact pattern language for describing sets of strings. Instead of writing loops and conditionals to find or validate text, you write one expression made of literals (characters that match themselves), character classes like \d for a digit, quantifiers like + for "one or more," and anchors like ^ and $ for the start and end of the string. A regex engine then walks that pattern across your text and reports where it matches.

Why does .* match more than I expect?

Because * and + are greedy by default: they consume as many characters as possible, then backtrack only if the rest of the pattern fails. On the input <b>bold</b>, the pattern <.*> matches the entire string, not just <b>, because .* grabs everything up to the last >. Add a ? to make the quantifier lazy (<.*?>) so it stops at the first > and matches just <b>.

What is the difference between a capturing and a non-capturing group?

Parentheses (...) create a capturing group: the matched text is stored and numbered (group 1, group 2, ...) so you can reuse it with backreferences like \1 or in a replacement. (?:...) is a non-capturing group โ€” it still groups tokens for a quantifier or alternation but does not store the result, which keeps your group numbers clean and is marginally faster.

How do I match a literal dot, plus sign, or parenthesis?

Escape it with a backslash. . means "any character," so to match an actual period you write \.. The same applies to other metacharacters: \+, \?, \*, \(, \), \[, \{, and \\ for a literal backslash. Inside a character class [...], most metacharacters are already literal, so [.] also matches just a period.

What does a word boundary \b actually match?

\b is a zero-width assertion: it matches the position between a word character (\w, i.e. [A-Za-z0-9_]) and a non-word character, without consuming any characters. \bcat\b matches "cat" as a standalone word but not the "cat" inside "category" or "concatenate," because those neighboring letters are word characters.

Are regular expressions the same in every programming language?

The core syntax (literals, classes, quantifiers, anchors, groups) is nearly universal, but flavors differ. PCRE (PHP, Perl), JavaScript, Python's re, Java, and .NET share most features, while POSIX tools like grep and sed use an older basic/extended syntax. Lookbehind support, named groups, and Unicode handling vary the most, so test your pattern in the target language before shipping it.

Should I validate email addresses with a regex?

For a quick sanity check, yes โ€” a pattern like ^[^@\s]+@[^@\s]+\.[^@\s]+$ catches obviously malformed input. But no regex fully implements the RFC 5322 email grammar, and truly verifying an address requires sending mail or checking MX records. Use regex to reject garbage, not to guarantee deliverability.

What is catastrophic backtracking and how do I avoid it?

Catastrophic backtracking happens when nested, overlapping quantifiers (like (a+)+$ on a long non-matching string) force the engine to try an exponential number of paths, freezing your program. Avoid it by making patterns specific, avoiding nested quantifiers on the same characters, using possessive quantifiers or atomic groups where supported, and anchoring patterns so the engine fails fast.

How do lookahead and lookbehind work?

They are zero-width assertions that check for context without including it in the match. foo(?=bar) matches "foo" only when it is followed by "bar"; foo(?!bar) matches "foo" only when it is not; (?<=foo)bar matches "bar" preceded by "foo"; and (?<!foo)bar matches "bar" not preceded by "foo". They are ideal for password-complexity checks and split-free extraction.

regexprogrammingtext processingdeveloper tools