Chapter 2: Character Classes and Escapes
Goal: Match specific sets of characters like digits, letters, or any character except certain ones.
What is a Character Class?
A character class is a way to say “match one character from this set.” Think of it like a menu - you order one item from the available choices:
Pattern: /[aeiou]/
Meaning: Match any ONE vowel (a, e, i, o, or u)
Text: "hello"
Matches: "e", "o" (two separate matches)
Real-World Analogy
| Scenario | Character Class | What It Matches |
|---|---|---|
| Picking from a menu | [Cc]heese |
“Cheese” or “cheese” |
| Rolling dice | [1-6] |
Any number 1-6 |
| Password requirements | [A-Za-z0-9] |
Letters and numbers |
Basic Character Classes
The Square Brackets […]
Put characters inside [] to match any one of them:
// Match any vowel
preg_match('/[aeiou]/', 'hello'); // Match: yes ('e' and 'o')
// Match any consonant
preg_match('/[bcdfghjklmnpqrstvwxyz]/', 'hello'); // Match: yes ('h', 'l', 'l')
Character Ranges
Use - to specify a range of characters:
/[0-9]/ // Any digit (0 through 9)
/[a-z]/ // Any lowercase letter
/[A-Z]/ // Any uppercase letter
/[a-zA-Z]/ // Any letter (case-insensitive without flag)
Inside the Brackets
Inside [...], most special characters lose their power. A . in a class is a literal dot — no backslash needed:
preg_match('/[.]/', 'a.b'); // Match: yes ('.' — literal dot)
preg_match('/file[.]txt/', 'file.txt'); // Match: yes (equivalent to /file\.txt/)
Four characters stay special:
// ']' closes the class — escape it to match a literal ']'
'/[\]]/' // Matches ']'
// '\' stays the escape character — write \\ to match a literal backslash
// '-' still builds ranges — put it first or last to make it literal
'/[a-]/' // Matches 'a' or '-'
// '^' negates the class, but only in first position — elsewhere it is literal
'/[a^]/' // Matches 'a' or '^'
Try It
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
echo $regex->explain('/[aeiou]/');
// Output:
// Regex matches
// Character Class: any character in [ 'a', 'e', 'i', 'o', 'u' ]
echo $regex->explain('/[0-9]/');
// Output:
// Regex matches
// Character Class: any character in [ Range: from '0' to '9' ]
echo $regex->explain('/[a-zA-Z]/');
// Output:
// Regex matches
// Character Class: any character in [ Range: from 'a' to 'z', Range: from 'A' to 'Z' ]
Negated Character Classes
Use ^ inside [] to match anything except these characters:
// Match any non-digit
preg_match('/[^0-9]/', 'A1B2'); // Match: yes ('A', 'B')
// Match any character except a, b, or c
preg_match('/[^abc]/', 'def'); // Match: yes ('d', 'e', 'f')
Explanation
[^0-9]matches any single character that is not a digit.- In
"A1B2", the first match is"A".
Try It
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
echo $regex->explain('/[^0-9]/');
// Output:
// Regex matches
// Character Class: any character except [ Range: from '0' to '9' ]
Shorthand Character Classes
These shortcuts save typing for common character sets:
| Shorthand | Equivalent | Meaning |
|---|---|---|
\d |
[0-9] |
Any digit |
\D |
[^0-9] |
Any non-digit |
\w |
[a-zA-Z0-9_] |
Any word character |
\W |
[^a-zA-Z0-9_] |
Any non-word character |
\s |
[ \t\r\n\f\v] |
Any whitespace |
\S |
[^ \t\r\n\f\v] |
Any non-whitespace |
. |
(any character) | Any single character |
Examples
// Match a digit
preg_match('/\d/', 'Phone123'); // Match: yes ('1', '2', '3')
// Match a word character (letter, number, underscore)
preg_match('/\w/', 'hello_world'); // Match: yes (all characters)
// Match whitespace
preg_match('/\s/', "hello world"); // Match: yes (the space)
Try It
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
echo $regex->explain('/\d+/');
// Output:
// Regex matches
// Character Type: A digit: [0-9] (one or more times)
echo $regex->explain('/\w+/');
// Output:
// Regex matches
// Character Type: A word character: [a-zA-Z_0-9] (one or more times)
echo $regex->explain('/\s+/');
// Output:
// Regex matches
// Character Type: A whitespace character: [ \t\n\x0B\f\r] (one or more times)
Unicode Character Classes
Use \p{...} with the u flag to match Unicode characters:
// Match any Unicode letter
preg_match('/^\p{L}+$/u', 'cafe'); // Match: yes (all letters)
// Match any Unicode number
preg_match('/^\p{N}+$/u', '123'); // Match: yes (all numbers)
// Match emoji (symbols)
preg_match('/\p{Emoji}/u', 'Hello 👋'); // Match: yes ('👋')
Common Unicode Properties
| Property | Meaning | Example |
|---|---|---|
\p{L} |
Any letter | a, Z, ç, Ω |
\p{N} |
Any number | 1, 5, Ⅶ |
\p{P} |
Any punctuation | !, ,, 。 |
\p{S} |
Any symbol | $, €, © |
\p{Z} |
Any separator (spaces) | ` ` U+0020, U+00A0, U+2028 |
\p{C} |
Other (controls, formats, unassigned) | \x00, \x07 (BELL), U+200B |
One surprise worth memorizing: tab and newline are not \p{Z} — they are control characters, so they belong to \p{Cc} (a sub-group of \p{C}):
preg_match('/\p{Z}/u', "\t"); // Match: no (tab is a control character)
preg_match('/\p{Z}/u', "\n"); // Match: no (so is newline)
preg_match('/\p{Cc}/u', "\t"); // Match: yes
Try It
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
echo $regex->explain('/^\p{L}+$/u');
// Output:
// Regex matches (with flags: u)
// Anchor: the beginning of a line
// Unicode Property: any character matching "L" (one or more times)
// Anchor: the end of a line
Good Patterns vs Bad Patterns
Good: Clear and Specific
// Match digits only
'/\d+/'
// Match letters only (ASCII)
'/^[A-Za-z]+$/'
// Match letters only (Unicode)
'/^\p{L}+$/u'
// Match word characters
'/\w+/'
Bad: Common Mistakes
// [A-z] includes characters between Z and a in ASCII!
// This includes [ \ ] ^ _ `
'/^[A-z]+$/'
// Correct: [A-Za-z]
'/^[A-Za-z]+$/'
// Using . when you mean literal dot
preg_match('/file.txt/', 'myfile.txt'); // Won't work as expected!
// Correct: Escape the dot
preg_match('/file\.txt/', 'myfile.txt'); // Match: yes ("file.txt")
Anatomy: A Realistic Email Pattern
The classic local-part class shows this chapter’s rules working together:
Pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i
- Start anchor
^. - Local part:
[a-z0-9._%+-]+— letters, digits and._%+-. The.sits inside a class, so it matches a literal dot with no escaping, and the-at the end is a literal hyphen, not a range operator. - Literal
@. - Domain:
[a-z0-9.-]+— the trailing-is literal again. \.— outside a class, the dot must be escaped (the trap from “Bad: Common Mistakes” above).- TLD:
[a-z]{2,}. - End anchor
$, with theiflag covering uppercase.
This pattern matches email addresses like [email protected].
Exercises
Exercise 1: Match Different Character Types
Create patterns that match:
- Any single digit
- Any lowercase letter
- Any character that is NOT a letter
- Any Unicode emoji
// Solution 1
$pattern1 = '/\d/';
// or: '/[0-9]/'
// Solution 2
$pattern2 = '/[a-z]/';
// Solution 3
$pattern3 = '/[^A-Za-z]/';
// Solution 4
$pattern4 = '/\p{Emoji}/u';
Exercise 2: Validate Your Patterns
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
$tests = [
'/\d/' => 'Match a digit',
'/[a-z]/' => 'Match lowercase letter',
'/[^A-Za-z]/' => 'Match non-letter',
'/\p{Emoji}/u' => 'Match emoji',
];
foreach ($tests as $pattern => $description) {
$result = $regex->validate($pattern);
echo "$pattern: " . ($result->isValid ? "Valid" : "Invalid") . " - $description\n";
}
Exercise 3: Explain Each Pattern
use PHPRegex\Toolkit\Regex;
$regex = Regex::create();
$patterns = ['/\d/', '/[a-z]/', '/[^0-9]/', '/\w/', '/\s/'];
foreach ($patterns as $pattern) {
echo "\n$pattern\n";
echo $regex->explain($pattern) . "\n";
}
Key Takeaways
- Character classes
[]match one character from a set - Ranges
0-9,a-zdefine consecutive characters - Negation
[^...]matches anything except - Shorthands
\d,\w,\ssave time - Unicode
\p{...}requires theuflag - Escape special chars like
.with\to match literally
Common Errors
Error: Forgotten Flag for Unicode
// Wrong: Unicode properties need the 'u' flag
preg_match('/^\p{L}+$/', 'café'); // May not work!
// Correct: Add the 'u' flag
preg_match('/^\p{L}+$/u', 'café'); // Works!
Error: [A-z] Trap
// Wrong: [A-z] includes special characters!
preg_match('/^[A-z]+$/', 'test_test'); // Match: yes (includes _)
// Correct: Be explicit
preg_match('/^[A-Za-z]+$/', 'test_test'); // Match: no (no _)
Error: Forgetting to Escape Special Chars
// Wrong: . matches any character
preg_match('/price.99/', 'priceX99'); // Match: yes (X instead of .)
// Correct: Escape the dot
preg_match('/price\.99/', 'priceX99'); // Match: no
preg_match('/price\.99/', 'price.99'); // Match: yes
Recap
You now understand:
- Character classes and ranges
- Negated character classes
- Shorthand classes (\d, \w, \s)
- Unicode character properties
- Common pitfalls and how to avoid them