Skip to main content

Introduction to Regular Expressions — Common Patterns and Practical Examples

Toolsbase Editorial Team
(Updated: )
Regular ExpressionsProgrammingText ProcessingDevelopmentTutorial

What Are Regular Expressions?

Regular expressions (regex) are a special notation for describing patterns in strings. They are used in virtually every situation that requires string processing, including text searching, replacement, and validation.

Nearly all programming languages support regular expressions, so once you learn them, you can apply them across JavaScript, Python, Java, PHP, Go, and more.

History of Regular Expressions

The origins of regular expressions trace back to the 1950s, when mathematician and computer scientist Stephen Kleene devised a notation for describing regular languages as part of his work on finite automata theory.

In the 1960s, Ken Thompson — who would later co-create the C programming language and Unix — applied Kleene's theory to real software, building it into the ed text editor and the search command grep (Global Regular Expression Print). This is what turned regular expressions from an academic notation into a practical, everyday tool.

In the 1980s, POSIX (Portable Operating System Interface) standardized regular expression syntax, defining two flavors: BRE (Basic Regular Expressions) and ERE (Extended Regular Expressions).

Later, the Perl language, created by Larry Wall, dramatically expanded what regular expressions could express. Named capture groups, lookaheads and lookbehinds, and lazy quantifiers — features now taken for granted in modern regex — largely trace back to Perl. This extended syntax became known as PCRE (Perl Compatible Regular Expressions) and has since been adopted by many other languages, including PHP, Python, and Ruby.

Basic Metacharacters

Regular expressions are composed of ordinary characters and metacharacters (characters with special meanings). Let's start with the basic metacharacters.

Character Classes

Metacharacter Meaning Example Matches
. Any single character (except newline) a.c abc, a1c, a-c
\d Digit (0-9) \d{3} 123, 456
\D Non-digit \D+ abc, ---
\w Alphanumeric and underscore \w+ hello_123
\W Non-word character \W @, #,
\s Whitespace \s+ , \t, \n
\S Non-whitespace \S+ hello

Anchors

Anchors match positions rather than characters themselves.

Metacharacter Meaning Example Description
^ Start of line ^Hello Matches "Hello" at the start
$ End of line end$ Matches "end" at the end
\b Word boundary \bcat\b Matches "cat", not "catch"

Character Sets

Square brackets [] let you define custom character classes.

[abc]       # Any of a, b, or c
[a-z]       # Any lowercase letter
[A-Za-z]    # Any letter (upper or lower)
[0-9]       # Any digit (equivalent to \d)
[^0-9]      # Any non-digit (^ means negation)
[a-zA-Z0-9_] # Alphanumeric and underscore (equivalent to \w)

Escaping Special Characters

Because . * + ? ^ $ { } [ ] ( ) | \ are all metacharacters, you need to escape them with a backslash \ whenever you want to match them literally.

# Match a literal period
3\.14      # Matches "3.14" (not "3X14")

# Match literal parentheses
\(hello\)  # Matches "(hello)"

Quantifiers

Quantifiers specify how many times the preceding element should repeat.

Quantifier Meaning Example Matches
* 0 or more ab*c ac, abc, abbc
+ 1 or more ab+c abc, abbc
? 0 or 1 colou?r color, colour
{n} Exactly n \d{4} 2026
{n,} n or more \d{2,} 12, 123, 1234
{n,m} Between n and m \d{2,4} 12, 123, 1234

Greedy vs. Lazy Matching

By default, quantifiers are "greedy" — they match as many characters as possible. Adding ? after a quantifier makes it "lazy," matching as few as possible.

# Input: <div>hello</div>
<.*>      # Greedy: matches <div>hello</div> entirely
<.*?>     # Lazy: matches <div> only

Lazy matching is particularly useful when extracting HTML tags or quoted strings.

When to use each:

  • Greedy matching — when you want to capture as much as possible, such as grabbing everything up to the last occurrence of a delimiter (e.g., the non-extension part of a file path)
  • Lazy matching — when you want to stop at the first closing delimiter, such as text inside HTML tags or content between quotation marks

Grouping and Capturing

Parentheses () group patterns together and capture (extract) the matched portions.

Basic Grouping

(abc)+      # One or more repetitions of "abc"
(red|blue)  # "red" or "blue"

Using Capture Groups

const pattern = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2026-03-08".match(pattern);

console.log(match[1]); // "2026" (year)
console.log(match[2]); // "03"   (month)
console.log(match[3]); // "08"   (day)

Named Capture Groups

The (?<name>...) syntax lets you assign names to groups, greatly improving readability.

const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2026-03-08".match(pattern);

console.log(match.groups.year);  // "2026"
console.log(match.groups.month); // "03"
console.log(match.groups.day);   // "08"

Backreferences

Capture groups can be referenced later in the same pattern using \1, \2, and so on — known as backreferences. They let you detect patterns where the same text repeats.

# Detect consecutive duplicate words (e.g., "the the")
\b(\w+)\s+\1\b

# Match a string that opens and closes with the same quote character
(['"]).*?\1
# Matches "hello" or 'world' (opening and closing quotes match)

Backreferences are also useful with JavaScript's replace.

// Convert a date from YYYY-MM-DD to DD/MM/YYYY
const result = "2026-03-08".replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
  "$<day>/$<month>/$<year>"
);
console.log(result); // "08/03/2026"

Non-Capturing Groups

When you don't need to save the match result, (?:...) creates a non-capturing group. This offers a slight performance advantage.

(?:http|https)://   # Groups the protocol part without capturing it

Lookahead and Lookbehind

Lookaheads and lookbehinds specify conditions about surrounding text without including it in the match result.

Syntax Name Description
(?=...) Positive lookahead Position followed by the pattern
(?!...) Negative lookahead Position not followed by the pattern
(?<=...) Positive lookbehind Position preceded by the pattern
(?<!...) Negative lookbehind Position not preceded by the pattern
# Match digits followed by "px" (without including "px" in the match)
\d+(?=px)

# Input: "100px 200em 300px"
# Matches: "100", "300"
# Match digits preceded by "$"
(?<=\$)\d+

# Input: "$100 200 $300"
# Matches: "100", "300"

Lookaheads and lookbehinds really prove their worth in situations where you need to match a string but exclude it whenever it's followed by something specific.

// Replace "color" with "colour", but skip the "color" inside "colorful"
const text = "This color is colorful.";
const result = text.replace(/color(?!ful)/g, "colour");
console.log(result); // "This colour is colorful."

Practical Pattern Examples

Email Address Validation

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

This pattern validates common email address formats. However, full RFC 5321 compliance would require a much more complex pattern. In practice, a simple format check combined with a confirmation email is the recommended approach.

Phone Number (US Format)

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

This pattern handles various US phone number formats with or without country code, parentheses, and different separators.

URL Validation

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

ZIP Code (US)

^\d{5}(-\d{4})?$

Matches both "12345" and "12345-6789" formats.

IPv4 Address

^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$

This strictly validates that each octet falls within the 0–255 range. 25[0-5] covers 250–255, 2[0-4]\d covers 200–249, and [01]?\d\d? covers 0–199.

Date (YYYY-MM-DD Format)

^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$

This restricts the month to 01–12 and the day to 01–31. That said, accounting for the actual number of days in February or for leap years is difficult with regex alone — in practice, it's more reliable to hand date parsing off to a dedicated library.

Password Strength Check

Validates that a password is at least 8 characters long and contains at least one uppercase letter, one lowercase letter, and one digit.

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

This pattern uses three positive lookaheads to independently check for the presence of each character type.

Regex Engine Differences Across Languages

The basic regex syntax is shared across most languages, but each engine has its own quirks and extensions worth knowing about.

Feature JavaScript Python (re) Java PCRE (PHP, etc.)
Named capture groups (?<name>...) (?P<name>...) (?<name>...) (?<name>...)
Variable-length lookbehind Not supported (ES2018 added fixed-length only) Supported Supported Supported (PCRE2)
Unicode property \p{L} Supported with the u flag Supported Supported Supported in u mode
Possessive quantifiers a++ Not supported Not supported Supported Supported
Inline modifiers (?i) Not supported (flags only) Supported Supported Supported
Global search g flag re.findall() Matcher.find() loop preg_match_all()

Key things to watch out for:

  • JavaScript attaches flags (g, i, m, s, u, v) outside the pattern, unlike most other languages. ES2018 added limited lookbehind support, but variable-length lookbehind is still unsupported.
  • Python's named capture groups use the (?P<name>...) syntax, and you reference them later with (?P=name).
  • Java's String.matches() checks whether the pattern matches the entire string, so ^...$ anchors aren't necessary — it behaves like fullmatch in other languages.

When porting a pattern to a different language, it's worth checking the regex cheat sheet for language-specific syntax differences.

Regular Expression Performance

Regular expressions are powerful, but a poorly designed pattern can cause serious performance problems.

Catastrophic Backtracking

Most regex engines rely on a backtracking strategy: when a match attempt fails at a given position, the engine steps back and tries a different path. This is usually fast, but certain pattern designs can cause the number of paths the engine tries to grow exponentially — a phenomenon known as catastrophic backtracking.

# A dangerous pattern: nested quantifiers
(a+)+b

# Given the input "aaaaaaaaaaaac", the engine tries an exponential
# number of combinations, causing a timeout or a frozen process

This problem is known as ReDoS (Regular Expression Denial of Service) and can become a genuine security vulnerability in web applications. Both Cloudflare and Stack Overflow have suffered production outages caused by exactly this kind of pattern in the past.

How to avoid it:

# Bad: nested quantifiers
(a+)+

# Good: atomic group (available in PCRE)
(?>a+)+

# Good: possessive quantifier (available in Java and PCRE)
a++

In environments like JavaScript, where possessive quantifiers and atomic groups aren't available, the safest approach is to redesign the pattern itself to eliminate overlapping search paths.

Performance Optimization Checklist

  • Avoid unnecessary capturing: use (?:...) for groups you don't need to extract
  • Prefer character classes: [abc] is more efficient than a|b|c
  • Use anchors: ^ and $ let the engine fail fast
  • Be specific: \w+ or [a-z]+ matches faster than a vague .+
  • Break up complex patterns: instead of trying to handle everything in a single regex, split the logic into multiple steps

Use the regex tester to visualize how a pattern executes — checking the step count is a great way to spot bottlenecks.

Regular Expression Flags

Most languages let you attach flags (options) to a regex to change how it behaves.

Flag JavaScript Python Description
Case-insensitive i re.IGNORECASE Makes [a-z] behave like [A-Za-z]
Multiline mode m re.MULTILINE ^ and $ match the start/end of each line
Dot-all mode s re.DOTALL . also matches newline characters
Global g Returns all matches (equivalent to findall)
Unicode u Enables Unicode properties like \p{L}
// The i flag makes matching case-insensitive
const pattern = /hello/i;
pattern.test("Hello World"); // true
pattern.test("HELLO");       // true

// The m flag makes ^ match the start of each line
const multiline = /^start/m;
"line1\nstart here".match(multiline); // ["start"]

Conclusion and Learning Path

Regular expressions are a versatile, all-purpose tool for text processing. Once you have a solid grasp of metacharacters, quantifiers, grouping, and lookaheads, you'll be equipped to handle most real-world text-processing challenges.

An efficient path to mastering regex:

  1. Start with the basic metacharacters (., \d, \w, \s) and quantifiers (*, +, ?)
  2. Get comfortable with capture groups and character classes []
  3. Sharpen your patterns with anchors (^, $, \b)
  4. Tackle more advanced conditions with lookaheads and lookbehinds
  5. Keep performance in mind as you optimize your patterns

It might feel overwhelming at first, but building up your vocabulary one common pattern at a time will get you there. The regex tester is a great way to check pattern behavior in real time as you learn, and the regex cheat sheet is a handy reference whenever you need to look up a metacharacter or piece of syntax.


References