# Regular Expressions, Explained

> A regex is a pattern that describes the shape of text - not code you run, but text you describe. Learn the mental model, the small toolkit you'll use 90% of the time, and how to avoid the classic traps.


---

# Regular Expressions, Explained

You've seen them: a wall of slashes, backslashes, and dollar signs like `^\d{4}-\d{2}-\d{2}$` -
and your stomach sank a little. Regular expressions have a reputation for being write-only magic
that only wizards understand. That reputation is undeserved. A regex is one small idea wearing a
scary costume, and once you see the idea, the costume stops working on you.

This guide makes regex readable instead of terrifying. We'll start with what a regex *actually is* (a
pattern that describes a *shape* of text), learn the handful of pieces you'll reach for almost every
time, and then meet the real-world traps - greedy matching, escaping, and regex that turns into
gibberish - so they don't bite you.

## How to read this

- **Just need to recognize the pieces?** Skim [Phase 2: The Core Toolkit](02-the-core-toolkit.md) -
  it's a tour of every symbol you'll actually use, each with a tiny example.
- **Want it to finally make sense?** Read in order. Each phase builds on the last, and the whole
  thing rests on the one idea in Phase 1.

## The phases

1. **[What a Regex Actually Is](01-what-a-regex-actually-is.md)** - the mental model: you're
   describing the *shape* of text, not writing code. With a tiny first example you can see match
   and not-match against.
2. **[The Core Toolkit](02-the-core-toolkit.md)** - the pieces you'll use 90% of the time:
   literals, character classes, quantifiers, anchors, and groups - built up to matching something
   real, and a straight word on why "the perfect email regex" is a trap.
3. **[Using Regex for Real (and the Gotchas)](03-using-regex-for-real.md)** - where you meet regex
   (editors, `grep`, code), and the classic traps: greedy vs lazy matching, escaping special
   characters, and regex becoming unreadable - with the cure for each.

> Deeper material - lookahead/lookbehind, backreferences, the differences between regex flavors
> (PCRE vs JavaScript vs POSIX), and catastrophic backtracking - is deliberately left for a
> follow-up guide. You can do an enormous amount of real work with only what's here.

Related reading: [Programming From Zero](/guides/programming-from-zero) for the basics underneath
this, and [The Terminal and Shell](/guides/the-terminal-and-shell) for using regex with tools like
`grep`.


---

# What a Regex Actually Is

Before any symbols, let's fix the one idea the whole topic rests on. Get this, and every confusing
pattern you ever see afterward becomes something you can reason about instead of fear.

Here's the shift: **a regex is not code you run. It's a description you write.** You're not telling
the computer *how* to search the text step by step. You're describing the *shape* of the thing
you're looking for, and handing that description to a piece of software - called a **regex engine**
- that does the actual searching for you.

📝 **Terminology.** A **regular expression** (everyone says "regex," some say "regexp") is a small
pattern that describes a set of strings. A **regex engine** is the built-in machinery - inside your
editor, inside `grep`, inside your programming language - that takes your pattern and checks text
against it. You write the pattern; the engine does the work.

## The mental model: you're describing text

Think about how you'd describe a phone number to a friend over the phone: "three digits, a dash,
then four digits." You didn't write instructions for scanning a page. You described the *shape* -
and a human listening could now recognize one on sight.

A regex is exactly that description, written in a compact notation the engine understands. The
notation looks alien at first, but the *job* is the friendly one you already do in plain language:
**say what the text looks like.**

```mermaid
flowchart LR
  Pattern[You write a PATTERN<br/>the regex] --> Engine[regex engine]
  Text[Some TEXT to search] --> Engine
  Engine --> Result[matches / no match]
```

*What this picture says:* you supply two things - a pattern and some text - and the engine reports
back where (and whether) the pattern's shape appears in the text. You never wrote a loop; you
described, and it matched.

## Why people get this wrong

The common wrong picture is that a regex is a *command* - something that "does" an action like
"find and delete all the emails." It isn't. On its own, a regex only answers one question: **does
this shape appear in this text, and if so, where?**

The *actions* - search, highlight, replace, extract - come from the tool you hand the regex to. Your
editor's Find box uses it to decide what to highlight. A replace feature uses it to decide what to
swap out. `grep` uses it to decide which lines to print. The regex itself is always the same humble
thing: a description of a shape. Keeping that separation in your head - *pattern describes, tool acts*
- stops a lot of confusion later.

## A tiny first example

The simplest possible regex is plain text that means itself. The pattern `cat` describes a very
specific shape: the letter `c`, then `a`, then `t`, side by side.

Let's see what it matches. Imagine asking the engine to find `cat` in a few different lines:

```text
  pattern:  cat

  "the cat sat"          ►  MATCH      (found c-a-t at position 4)
  "a category error"     ►  MATCH      (found c-a-t inside "category")
  "concatenate"          ►  MATCH      (found c-a-t inside "concatenate")
  "a dog barked"         ►  no match   (no c-a-t shape anywhere)
  "CAT scan"             ►  no match   (uppercase - different characters)
```

*What just happened:* the engine slid the shape `cat` along each line, looking for three characters
in a row that match. It found that shape inside `category` and `concatenate` - because by default a
regex matches *anywhere in the text*, not only whole words. It found nothing in `a dog barked`
(no such run of letters), and nothing in `CAT scan` because, by default, matching is
**case-sensitive** - `C` and `c` are different characters to the engine.

⚠️ **Gotcha - "match" means "found somewhere," not "is equal to."** The most common beginner
surprise is expecting `cat` to match only the exact word "cat." It doesn't. A match means the shape
was found *somewhere inside* the text. Matching a *whole* string, or a *whole word*, takes extra
pieces (anchors and boundaries) that we'll meet in the next two phases. For now: a bare pattern
finds its shape wherever it appears.

💡 **Key point.** A regex describes a shape of text. The engine searches; the tool acts. Everything
in the next phase is only richer ways to describe a shape - "any digit" instead of one specific
digit, "one or more of these" instead of exactly one. The idea never changes.

## Why this saves you later

Once you hold "it's a description, not a command," the scary patterns lose their teeth. When you
meet `^\d{4}-\d{2}-\d{2}$` in the wild, you won't see a spell - you'll know it's *describing a
shape* ("four digits, dash, two digits, dash, two digits, and nothing else"), and you'll be able to
read it left to right like a sentence. That's the whole game: regex is readable once you know it's
a description. Phase 2 hands you the vocabulary.

## Recap

1. A **regex** is a pattern that describes the *shape* of text - a description, not a command.
2. A **regex engine** (in your editor, `grep`, or language) does the actual searching against your
   pattern.
3. The *tool* you give the regex to decides the action (highlight, replace, filter); the regex only
   describes.
4. A bare pattern like `cat` matches its shape **anywhere** in the text, and matching is
   **case-sensitive** by default.
5. "Match" means "this shape was found somewhere," not "the text equals this."


---

# The Core Toolkit

Now that you know a regex *describes a shape*, you need a vocabulary for describing shapes more
richly than "these exact letters." This phase is that vocabulary, and the good news is it's small.
There are dozens of regex features in the world, but a handful do almost all the work.

We'll go piece by piece, each with a tiny example, then assemble them into something real at the end.

## Literals - characters that mean themselves

You already met these in Phase 1. Most characters in a regex are **literals**: they match exactly
themselves. `a` matches an `a`, `7` matches a `7`, `-` matches a dash.

```text
  pattern:  dog

  "good dog"   ►  MATCH   (the literal run d-o-g)
  "doge"       ►  MATCH   (d-o-g appears, then more)
  "cat"        ►  no match
```

*What just happened:* nothing fancy - each character stood for itself, in order. Literals are the
floor everything else builds on; the special power comes from the few characters that *don't* mean
themselves.

## Character classes - "any one of these"

Often you don't want one exact character; you want "any digit" or "any letter." That's a
**character class** - a description of a *set* of characters, any one of which matches a single
position.

The everyday shortcuts:

```text
  \d   any digit                 0 1 2 3 4 5 6 7 8 9
  \w   any "word" character      letters, digits, and underscore (a–z A–Z 0–9 _)
  \s   any whitespace            space, tab, newline
```

📝 **Terminology.** A **character class** matches *exactly one* character - one position in the
text. `\d` doesn't mean "a number," it means "one digit." To match several, you'll add a quantifier
(next section).

```text
  pattern:  \d

  "room 7"     ►  MATCH   (the 7)
  "no number"  ►  no match (not a single digit present)
```

*What just happened:* `\d` matched the single character `7`. It would also have matched the `3` in
`B3` or the `0` in `2026` - any one digit, wherever it sits.

You can also build your *own* class with square brackets. `[aeiou]` means "any one of these
vowels." A range with a dash, like `[a-f]`, means "any one character from a to f."

```text
  pattern:  [aeiou]

  "rhythm"     ►  no match (no a/e/i/o/u)
  "cat"        ►  MATCH   (the a)
```

*What just happened:* the brackets describe a custom set, and the engine matched the first character
that fell inside it - the `a` in `cat`. ⚠️ **Gotcha - a dash inside brackets is a range.** `[a-z]`
means "a through z," *not* "the letters a, dash, z." To match a literal dash, put it first or last:
`[-az]` or `[az-]`. This trips up everyone once.

## Quantifiers - "how many"

A character class matches one position. **Quantifiers** say how many times the thing right before
them may repeat. These four cover almost everything:

```text
  *      zero or more   (any amount, including none)
  +      one or more    (at least one)
  ?      zero or one     (optional)
  {n}    exactly n times
```

```text
  pattern:  \d+

  "room 7"       ►  MATCH on "7"      (one or more digits)
  "year 2026"    ►  MATCH on "2026"   (grabs all four - "one or more" keeps going)
  "no digits"    ►  no match
```

*What just happened:* `\d+` means "one or more digits in a row." On `year 2026` it didn't stop at
the first digit - it kept matching as long as the next character was also a digit, capturing the
whole `2026`. That "keep going" behavior is important, and it's the seed of the *greedy matching*
trap we'll meet in Phase 3.

A quick tour of the others:

```text
  pattern:  colou?r        the u is optional (? = zero or one)

  "color"        ►  MATCH   (zero u's)
  "colour"       ►  MATCH   (one u)
  "colouur"      ►  no match (two u's - ? allows at most one)
```

*What just happened:* `?` made the `u` optional, so one pattern matched both the US and British
spellings - a tiny taste of why regex is worth it: one description, several real-world variations.

## Anchors - "where in the text"

By default a pattern matches anywhere (Phase 1). **Anchors** pin a pattern to a position instead of
matching a character:

```text
  ^   the start of the line/text
  $   the end of the line/text
```

Anchors match a *position*, not a character - think of them as "right here at the edge."

```text
  pattern:  ^cat$         start, then c-a-t, then end - nothing else allowed

  "cat"          ►  MATCH   (the whole text is exactly "cat")
  "category"     ►  no match (there's more after "cat", so it's not the end)
  "the cat"      ►  no match (there's text before "cat", so it's not the start)
```

*What just happened:* wrapping `cat` in `^...$` turned "find cat anywhere" into "the text must be
*exactly* cat." This is the answer to the Phase 1 gotcha - anchors are how you demand a *whole*
match instead of a found-somewhere match. ⚠️ **Gotcha - `^` means two different things.** At the
*start* of a pattern, `^` is the start-anchor. But *inside square brackets*, `[^abc]` means "any
character *except* a, b, or c" - a negation. Same symbol, totally different job depending on where
it sits. Read carefully.

## Groups - "treat this as one unit"

Parentheses **group** part of a pattern so a quantifier (or other operator) applies to the whole
group, not one character. They also "capture" what matched, so a tool can pull it out later.

```text
  pattern:  (ab)+         "ab" repeated one or more times

  "ababab"       ►  MATCH   (three ab's in a row)
  "abc"          ►  MATCH on "ab"  (one ab, then c stops it)
  "ba"           ►  no match
```

*What just happened:* without the parentheses, `ab+` would mean "an `a`, then one or more `b`s." The
group made `+` apply to the *pair* `ab`. Grouping describes repeated *structures*, not only repeated
single characters - and capturing is how search-and-replace knows which piece to keep.

## Building something real: a simple date

Now assemble the toolkit. A date like `2026-06-19` has a clear shape: four digits, a dash, two
digits, a dash, two digits - and we want the *whole* string to be that and nothing else.

```text
  pattern:  ^\d{4}-\d{2}-\d{2}$

  "2026-06-19"   ►  MATCH
  "26-6-19"      ►  no match (\d{4} needs four digits, \d{2} needs two)
  "2026-06-19x"  ►  no match ($ demands the end right after the last digit)
```

*What just happened:* read it left to right like a sentence. `^` (start), `\d{4}` (exactly four
digits), `-` (a literal dash), `\d{2}` (exactly two digits), `-`, `\d{2}`, `$` (end). That wall of
symbols from the guide's intro is now a plain English description you can read. That's the payoff of
the toolkit.

💡 **Key point.** This pattern checks the *shape*, not the *meaning*. It happily matches
`9999-99-99`, which is not a real date. Regex describes how text *looks*, not whether it's
*sensible*. Knowing where that line falls - and not asking regex to cross it - is half of using it
well.

## A straight word: the "perfect email regex" is a trap

A rite of passage is trying to write one regex that matches *every* valid email address and rejects
every invalid one. Don't. The rules for what's technically a valid email are genuinely strange and
sprawling, and the "complete" regex people pass around is hundreds of characters long, unreadable, and
*still* not fully correct.

What you actually want in real life is a *good-enough* shape check - "looks roughly like an email" -
and then to confirm it's real by sending a message to it. A practical, readable pattern:

```text
  pattern:  ^\S+@\S+\.\S+$

  "ada@example.com"   ►  MATCH
  "ada@localhost"     ►  no match (no dot after the @)
  "not an email"      ►  no match (no @)
```

*What just happened:* `\S` means "any non-whitespace character" (the uppercase counterpart of `\s`).
So this reads: start, some non-spaces, an `@`, some non-spaces, a literal dot (`\.` - escaped, because
a bare `.` is special; more in Phase 3), some non-spaces, end. It's not airtight, and it's not meant to
be - it catches obvious typos and stays readable, which is the right trade. Chasing perfection here is
how regex earns its scary reputation.

## Recap

1. **Literals** match themselves; they're the floor everything builds on.
2. **Character classes** match one character from a set: `\d` (digit), `\w` (word char), `\s`
   (whitespace), or your own `[...]`.
3. **Quantifiers** say how many: `*` (zero or more), `+` (one or more), `?` (optional), `{n}`
   (exactly n).
4. **Anchors** pin to a position: `^` (start), `$` (end) - this is how you demand a *whole* match.
5. **Groups** `(...)` apply a quantifier to a whole unit and capture what matched.
6. Regex checks **shape, not meaning** - and "the perfect email regex" is a trap; aim for
   good-enough and readable.

## Try it yourself

Edit the pattern or the sample text and watch the matches highlight live:

```playground-regex
\b[\w.]+@[\w.]+\.\w+\b
Email alice@example.com or bob@test.org - but "hello world" matches nothing.
```

## Practice

```exercise
[
  {
    "type": "regex",
    "task": "Write a regex that matches a date shaped like `2026-06-19` - four digits, a dash, two digits, a dash, two digits - and nothing else, not just a piece of a longer string.",
    "mustMatch": ["2026-06-19", "1999-01-01", "0000-12-31"],
    "mustNotMatch": ["26-6-19", "2026/06/19", "date: 2026-06-19", "2026-06-19 note"],
    "hint": "Anchor it with ^ and $ so the whole string has to match, not just part of it."
  }
]
```


---

# Using Regex for Real (and the Gotchas)

You've got the toolkit. Now let's put it where you'll actually use it, and walk straight into the
three traps that catch everyone, so you see them coming. Most people learn these the hard way, losing
an afternoon to a pattern that "should work." You don't have to.

## The cheat-card: symptom → calm fix

When a regex misbehaves, it's almost always one of these. Scan here first.

| Symptom | Likely cause | Calm fix |
|---|---|---|
| Pattern grabs *way too much* text | Greedy quantifier (`.*`) | Make it lazy: `.*?` - see [Greedy vs lazy](#trap-1-greedy-vs-lazy-matching) |
| `.` or `(` or `.` "isn't working" | It's a *special* character | Escape it with a backslash: `\.` `\(` - see [Escaping](#trap-2-escaping-special-characters) |
| Pattern works but nobody can read it (including you, next week) | Regex is write-only | Build it incrementally and test on samples - see [Write-only](#trap-3-regex-becomes-write-only) |
| Pattern matches nothing | Often a missing escape, or a wrong assumption about case | Test it in a regex tester against a real sample |

Now the details.

## Where you actually meet regex

Regex isn't one tool - it's a notation that shows up *inside* many tools. The three you'll hit first:

**Your editor's Find box.** VS Code, Sublime, JetBrains IDEs, and most others have a little `.*`
button in their search bar. Click it and your search becomes a regex - "find every `TODO` followed by
a colon" or "find all four-digit numbers" is one search instead of fifty.

**Find-and-replace with capture groups.** This is where groups (Phase 2) pay off. You can match a
shape, capture pieces of it, and rebuild them in the replacement. In most editors a captured group
is referred to in the replacement as `$1`, `$2`, and so on:

```text
  find:     (\w+)@(\w+)
  replace:  $2 owns $1

  "ada@example"  ►  becomes  "example owns ada"
```

*What just happened:* the two groups captured `ada` and `example`; the replacement put them back in a
new order using `$1` and `$2` - how a single search-replace can restructure hundreds of lines safely.

**`grep` on the command line.** `grep` ("global regular expression print") filters lines of text by
a pattern - the original regex tool, still everywhere.

```console
$ grep "ERROR" server.log
2026-06-19 14:02:11 ERROR database connection refused
2026-06-19 14:02:12 ERROR retry failed
```

*What just happened:* `grep` walked the file line by line and printed only the lines where the
pattern `ERROR` matched. The pattern can be any regex - `grep "^\d{4}-"` would print only lines
starting with a four-digit year. (For more on `grep` and friends, see
[The Terminal and Shell](/guides/the-terminal-and-shell).)

**In code.** Every mainstream language has regex built in - `re` in Python, `RegExp` in JavaScript, and
so on. The notation is mostly the same across them; the function names differ. The same shape you
typed into your editor's Find box works there too.

## Trap 1: greedy vs lazy matching

The single most common "why is my regex eating everything?" bug. By default, quantifiers like `*` and
`+` are **greedy**: they match *as much as they possibly can* while still letting the overall pattern
succeed.

Say you want to pull the first `<tag>` out of some text:

```text
  pattern:  <.*>
  text:     "<b>bold</b>"

  you expected:  "<b>"
  you got:       "<b>bold</b>"   ◄── the whole thing!
```

*What just happened:* `.*` means "any characters, as many as possible." Being greedy, it gobbled
everything from the first `<` all the way to the *last* `>` it could find, because that still leaves a
valid match. It didn't stop at the first `>`; it stopped at the last one.

The fix is a **lazy** quantifier: add a `?` after it to mean "as *few* as possible."

```text
  pattern:  <.*?>
  text:     "<b>bold</b>"

  you got:  "<b>"   ◄── stops at the first >
```

*What just happened:* `.*?` matched the fewest characters needed to reach a `>`, so it stopped at the
first one. ⚠️ **Gotcha - `?` does two different jobs.** On its own (`u?`) it means "optional" (Phase
2). Placed *after another quantifier* (`*?`, `+?`) it means "lazy." Same symbol, different role
depending on position - much like `^` from Phase 2. When a pattern grabs too much, "make it lazy" is
your first move.

## Trap 2: escaping special characters

Some characters are *special* in regex - they do a job rather than matching themselves. You've met
several: `. * + ? ( ) [ ] { } ^ $ \ |`. The trap is wanting to match one of them *literally*. The big
one is the dot: a bare `.` means "any single character," not a literal period.

```text
  pattern:  3.14
  text:     "3x14"

  you expected:  no match (you wanted a real dot)
  you got:       MATCH    ◄── "." matched the "x"!
```

*What just happened:* `.` matched *any* character, including `x`, so `3x14` matched a pattern you
meant for `3.14`. To match a literal dot, **escape** it with a backslash:

```text
  pattern:  3\.14
  text:     "3x14"   ►  no match
  text:     "3.14"   ►  MATCH
```

*What just happened:* `\.` told the engine "I mean an actual period here, not any-character." The
backslash is the universal "treat the next character literally" switch - to match a literal `(`,
write `\(`; for a literal `$`, write `\$`.

📝 **Terminology.** **Escaping** means putting a backslash before a special character to strip its
power and make it match literally. When in doubt about whether a punctuation character is special,
escaping it is harmless for most punctuation - `\.` and `.` differ, but escaping a character that
*isn't* special usually matches it literally anyway.

🪖 **War story.** A classic 2am bug: someone writes a pattern to find IP addresses like
`192.168.0.1` using `\d+.\d+.\d+.\d+`, ships it, and weeks later it's quietly matching lines like
`12x45y67z89` because every `.` was an "any character." The fix was four backslashes:
`\d+\.\d+\.\d+\.\d+`. The dot is the most-forgotten escape in all of regex - when you mean a literal
dot, escape it.

## Trap 3: regex becomes write-only

The trap that gives regex its bad name. A pattern you wrote fluently on Tuesday is total gibberish to
you on Friday. Regex packs a lot of meaning into very few characters, which makes it powerful - and
makes a long one genuinely hard to read, even for the person who wrote it.

The cure is not "get smarter." It's **process**:

- **Build incrementally.** Don't write the whole pattern at once. Start with the simplest piece that
  matches *something*, confirm it works on a real sample, then add one piece and re-check. For the
  date pattern, you'd start with `\d{4}`, confirm it grabs the year, then add `-\d{2}`, and so on.
  Each step you *see* working, so a mistake is one small addition away - not buried in a wall.

- **Test on real samples, in a regex tester.** A regex tester is a web page or editor panel where you
  paste your pattern and some sample text, and it highlights what matches *as you type*. Popular ones
  include regex101 and regexr - the difference between guessing and *seeing*. Always test against real
  data, both text that *should* match and text that *shouldn't*, before trusting a pattern in
  production.

- **Leave a comment.** When a regex lands in code, write one plain-English line above it saying what
  shape it describes: `# matches dates like 2026-06-19`. Future-you, and your teammates, will be
  grateful. The pattern says *how*; the comment says *what*.

- **Don't out-clever yourself.** If a pattern is getting monstrous (the "perfect email regex" urge from
  Phase 2), step back. Two simple regexes, or a simple regex plus a little ordinary code, often beats
  one heroic unreadable line.

💡 **Key point.** Greedy matching, missing escapes, and unreadable patterns cause the large majority
of regex pain - and all three have the same root cure: **test on real samples and build up one piece
at a time.** You don't write a perfect regex; you *grow* one, watching it match as you go.

## Recap

1. Regex lives *inside* tools: editor Find boxes, find-and-replace (with `$1` capture groups),
   `grep`, and code.
2. **Greedy** quantifiers (`.*`) grab as much as possible; add `?` to make them **lazy** (`.*?`) and
   stop at the first match.
3. **Special characters** (`. * + ?` and friends) do a job, not match themselves - **escape** them
   with a backslash (`\.`) when you want them literally. The forgotten dot is the classic bug.
4. Regex turns **write-only** when you write it all at once. The cure: build incrementally, test on
   real samples in a regex tester, and leave a comment.
5. Readable-and-correct beats clever-and-fragile. When a pattern gets monstrous, split it.

You now have the mental model, the everyday toolkit, and the traps mapped. That's enough to read and
write the regex you'll meet in real work - calmly, and without the dread.
