Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Regular Expressions  Module 2 – Characters, escapes and character classes

Character classes, ranges and negation

Match one character from a set with classes, ranges and negation, place ], - and ^ safely, and see where POSIX names and set operations work.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Go 1.27.1 and PHP 8.5.11
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build character classes with ranges and negation, and work out from code points what a range covers
  • Write ], - and ^ inside a class so that JavaScript, Python, PCRE2 and Go all read them as plain characters
  • Recognise where POSIX bracket expressions such as [[:alpha:]] work, and where they fail without an error
  • Use class set operations with the v flag of JavaScript and in PCRE2, and name the Java and .NET forms

Before you start

What you’ll learn · Before you start

What you will learn

  • Build character classes with ranges and negation, and work out from code points what a range covers
  • Write ], - and ^ inside a class so that JavaScript, Python, PCRE2 and Go all read them as plain characters
  • Recognise where POSIX bracket expressions such as [[:alpha:]] work, and where they fail without an error
  • Use class set operations with the v flag of JavaScript and in PCRE2, and name the Java and .NET forms

Before you start

On this page

A hex colour such as #C0FFEE has six positions, and each one may hold any of 22 characters: the ten digits, A to F and a to f. Writing the 22 choices out for every position would be unreadable; a character class: A part of a pattern between square brackets that matches exactly one character of the text: any one of the characters, ranges and classes listed inside it, such as [aeiou] for one vowel. says the same in eleven characters, [0-9A-Fa-f].

Below you will build classes from single characters and ranges, turn a class inside out with a caret, and meet the places where flavour: A particular regular-expression syntax and feature set, such as ECMAScript, Python re, PCRE2 or RE2; a pattern is written for one of them. More in “What a regular expression is and where it runs” part ways: JavaScript reads []a] as a class that can never match, [A-z] takes six symbols besides the letters, [[:alpha:]] means “a letter” in PCRE2 and Go but quietly matches nothing in JavaScript, and “a to z except the vowels” can be written as one class in some flavours only. Each example shows what its engine printed: JavaScript, Python, PCRE2, Go or PHP.

A class matches one character from a set

British English writes grey and American English gray. The pattern gr[ae]y finds both: g and r match themselves, then the class [ae] matches one character that is either a or e, then y matches the y. A class is one position of the pattern, never more: it does not match graey, where two letters stand between r and y, and it does not match gry, where there is none. The order of the members does not matter, so [ea] is the same class, and a member written twice changes nothing: [aa] is [a]. A class chooses between single characters; a choice between longer pieces, such as gray or grey or graey, is alternation with |, which a later module covers.

Inside the brackets the dot, the star and most other metacharacter: A character that carries a special meaning in a regex pattern. ECMAScript lists fourteen outside a character class: ^ $ \ . * + ? ( ) [ ] { } and |. A backslash before any one of them makes it literal. More in “Literals, metacharacters and the dot” are plain characters, so [.*] matches a dot or a star. A class can also hold the shorthand classes such as \d, which the next lesson covers.

A caret straight after the opening bracket turns the set inside out. [^0-9] is a negated class: A character class that starts with a caret right after the opening bracket, such as [^0-9]. It matches exactly one character that is not in the set, line breaks included.: one character that is not a digit. It is still one character, so it needs a character to look at, and it treats a line terminator: A character that ends a line. ECMAScript lists four (U+000A, U+000D, U+2028 and U+2029). Python, Go and PCRE2 treat the line feed alone as the line terminator by default. The dot refuses line terminators without a dotAll flag. More in “Literals, metacharacters and the dot” like any other character. That is the difference from the dot, which stops at line breaks. Run the program and compare the last two lines:

One position, one character: a class and its negation JavaScript · class-basics/class-basics.mjs
// A character class matches ONE character from its set; [^…] matches one character outside it.
function show(pattern, text) {
  const m = text.match(pattern);
  const found = m ? `${JSON.stringify(m[0])} at ${m.index}` : 'no match';
  console.log(`${pattern} in ${JSON.stringify(text)}: ${found}`);
}

// One position, two allowed letters: a or e.
for (const word of ['grey', 'gray', 'graey', 'gry']) show(/gr[ae]y/, word);

// Negation: the first character that is NOT a digit.
show(/[^0-9]/, '4021-7');
// A negated class still needs a character: after the 7 there is none.
show(/7[^0-9]/, '4021-7');

// The dot stops at a newline; a negated class does not.
show(/a.b/, 'a\nb');
show(/a[^x]b/, 'a\nb');

Output

/gr[ae]y/ in "grey": "grey" at 0
/gr[ae]y/ in "gray": "gray" at 0
/gr[ae]y/ in "graey": no match
/gr[ae]y/ in "gry": no match
/[^0-9]/ in "4021-7": "-" at 4
/7[^0-9]/ in "4021-7": no match
/a.b/ in "a\nb": no match
/a[^x]b/ in "a\nb": "a\nb" at 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node class-basics.mjs

[^0-9] found the hyphen at position 4, the first character that is not a digit. 7[^0-9] found nothing, because the 7 is the last character and nothing follows it for the class to take. And a[^x]b matched across the line break that stopped a.b. PCRE2’s documentation makes the same point for its own classes, with [^a] as the example: whatever line ending is in force, that class matches a line break.

Key idea

A class matches exactly one character at one position: any of its members, or, with a caret first, any character that is not a member, line breaks included.

Checkpoint

Check: A class matches one character

  1. Question 1 of 2 In which of these texts does gr[ae]y find a match?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: grey

    A class stands for one position of the text, and at that position it accepts any one of its members. gr[ae]y therefore matches grey and gray and nothing longer or shorter: graey has two letters where the class allows one, and gry has none.

  2. Question 2 of 2 /[^,]+/g collects every run of characters that are not commas. Which runs does it find in the text a,b, then a line break, then c,d?

    Choose one answer.

    A hint

    Ask what the class does with the line break, which is not a comma.

    Show the answer: Question 2 of 2

    Answer: "a", then "b\nc" (the line break inside it), then "d"

    [^,] matches any one character except the comma, and the + lets it repeat. Between the first and the second comma stand b, the line break and c, all of them outside the set, so they form one run. A class never treats a line terminator specially, which is the difference from the dot.

Ranges follow code points, not the alphabet

[A-z] looks like “any letter, capital or small”, and it accepts the underscore. A hyphen between two characters in a class makes a character range: Two characters joined by a hyphen inside a character class, such as a-z. It matches every character whose code point lies between the two, both included, so A-z also takes the six symbols between Z and a., and ECMAScript defines a range as every character whose value lies in the interval from the first to the last, both included. Python, PCRE2 and Go use the same rule. The value is the character’s code point: The number the Unicode standard assigns to a character, independent of how the character is stored in memory. More in “What a regular expression is and where it runs”, and in Unicode the capitals and the small letters are not neighbours: six symbols stand between Z and a, as the figure shows row by row.

Three rows of code points: A to Z (U+0041 to U+005A), six symbols [ \ ] ^ _ and backtick (U+005B to U+0060), then a to z.A to Z: U+0041 to U+005A, 26 lettersA41B42C43…Y59Z5ABetween Z and a: 6 more, not letters[5B\5C]5D^5E_5F`60a to z: U+0061 to U+007A, 26 lettersa61b62c63…y79z7A[A-z]: 58 code points. [A-Za-z]: 52.

What [A-z] covers: every code point from U+0041 to U+007A

Text description of the diagram

Three rows of boxes, each box a character with its code point in hexadecimal under it.

The first row is the capital letters A to Z, code points U+0041 to U+005A: A 41, B 42, C 43, then a gap for the letters D to X, then Y 59 and Z 5A. That is 26 letters.

The second row, drawn with dashed outlines, is the six characters that come next in Unicode, between Z and a: the opening square bracket 5B, the backslash 5C, the closing square bracket 5D, the caret 5E, the underscore 5F and the backtick (grave accent) 60. None of them is a letter.

The third row is the small letters a to z, code points U+0061 to U+007A: a 61, b 62, c 63, a gap for d to x, then y 79 and z 7A. That is 26 more letters.

The line at the bottom adds them up: the range A-z takes all 58 code points from U+0041 to U+007A, 26 + 6 + 26, while the two ranges A-Z and a-z together take only the 52 letters.

The figure lays out the 58 code points from U+0041 to U+007A. The program tests each character around the range against [A-z] and against [A-Za-z], the class that names its two ranges separately, then counts both over all of ASCII:

What [A-z] holds besides the letters JavaScript · range-az/range-az.mjs
// A range covers every code point from its first character to its last, so [A-z] holds more than letters.
const wide = /[A-z]/;
const letters = /[A-Za-z]/;
const hex = (ch) => 'U+' + ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0');
const yes = (re, ch) => (re.test(ch) ? 'yes' : 'no');

console.log('char  code point  [A-z]  [A-Za-z]');
for (const ch of '@AZ[\\]^_`az{') {
  console.log(`${ch.padEnd(6)}${hex(ch).padEnd(12)}${yes(wide, ch).padEnd(7)}${yes(letters, ch)}`);
}

// Count over all 128 ASCII characters.
let inWide = 0;
let inLetters = 0;
for (let cp = 0; cp < 128; cp++) {
  const ch = String.fromCodePoint(cp);
  if (wide.test(ch)) inWide++;
  if (letters.test(ch)) inLetters++;
}
console.log(`ASCII characters in [A-z]: ${inWide}, in [A-Za-z]: ${inLetters}`);

// A range must run upwards: a (U+0061) comes after Z (U+005A), so [a-Z] is refused.
try {
  new RegExp('[a-Z]');
  console.log('[a-Z] compiles');
} catch (e) {
  console.log(`[a-Z]: ${e.name}`);
}

Output

char  code point  [A-z]  [A-Za-z]
@     U+0040      no     no
A     U+0041      yes    yes
Z     U+005A      yes    yes
[     U+005B      yes    no
\     U+005C      yes    no
]     U+005D      yes    no
^     U+005E      yes    no
_     U+005F      yes    no
`     U+0060      yes    no
a     U+0061      yes    yes
z     U+007A      yes    yes
{     U+007B      no     no
ASCII characters in [A-z]: 58, in [A-Za-z]: 52
[a-Z]: SyntaxError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node range-az.mjs

[A-z] said yes to [, the backslash, ], ^, _ and the backtick, and [A-Za-z] said no to all six: 58 characters against 52. The last line shows the other way a range goes wrong. [a-Z] runs backwards, from U+0061 down to U+005A, and every flavour of this lesson refuses it when the pattern is compiled. Press Edit, put your own characters in the string the loop reads, such as a colon or an é, and run it again.

Unicode Character Inspector Paste any character to see its code point, so you can check what a range between two characters really covers.

The safe habit is one range per block of characters you want, all inside one class. The ends of a range may also be code-point escapes, the forms the lesson on literals and the dot compared: [\u0900-\u097F] is the whole Devanagari block in JavaScript and in Python. A hex digit is three blocks, so [0-9A-Fa-f] holds three ranges and nothing else. Written once per position after a #, it finds six-digit and three-digit colours:

Hex colours from one class per digit JavaScript · hex-colours/hex-colours.mjs
// Hex colours: one class for one hex digit, written once per digit.
const HEX = '[0-9A-Fa-f]';
const six = new RegExp('#' + HEX.repeat(6));
const three = new RegExp('#' + HEX.repeat(3));
console.log('six digits:  ', String(six));
console.log('three digits:', String(three));

const theme = ['#FF8800', '#0aF', '#12G4F0', '#fff', '#C0FFEE', '#1e90ff'];
for (const colour of theme) {
  const a = colour.match(six);
  const b = colour.match(three);
  console.log(`${colour.padEnd(8)} six: ${(a ? a[0] : '-').padEnd(8)} three: ${b ? b[0] : '-'}`);
}

Output

six digits:   /#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/
three digits: /#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/
#FF8800  six: #FF8800  three: #FF8
#0aF     six: -        three: #0aF
#12G4F0  six: -        three: -
#fff     six: -        three: #fff
#C0FFEE  six: #C0FFEE  three: #C0F
#1e90ff  six: #1e90ff  three: #1e9

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hex-colours.mjs

#12G4F0 failed both patterns at the G, and every six-digit colour matched. The three-digit pattern also found #FF8 at the start of #FF8800, because nothing tells it where a colour ends. A later module adds anchors and counted repetition, which stop that and write the six classes as one.

Key idea

A range takes every code point from its first character to its last, so give each block its own range, as in [A-Za-z], and check what lies between the ends.

Checkpoint

Check: Ranges cover code points

  1. Question 1 of 2 The digit 0 is U+0030 and the letter z is U+007A. How many ASCII characters does the class [0-z] match?

    Type a number.

    A hint

    Count every code point from the first to the last, both ends included.

    Show the answer: Question 1 of 2

    Answer: 75 characters

    0x7A is 122 and 0x30 is 48, so the range holds 122 − 48 + 1 = 75 code points: the 10 digits, the 7 symbols from the colon to @, the 26 capitals, the 6 symbols between Z and a, and the 26 small letters.

  2. Question 2 of 2 [+-.] was meant to match a plus, a hyphen or a dot. In the text 1,5-3.0+2 it matches one more character as well. Which one?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The comma

    Inside a class, a hyphen between two characters is a range from the first code point to the second. U+002B to U+002E covers four characters: + , - and the dot. JavaScript, Python and Go all return the comma, the hyphen, the dot and the plus from 1,5-3.0+2.

Inside a class, escape the bracket, the hyphen and the caret

Inside a class only four characters have a job, as Literals, metacharacters and the dot listed: ] ends the class, the backslash escapes, ^ negates when it comes first, and - makes a range when it stands between two characters. To match one of them as itself, the flavours offer two ways: a position where the job cannot apply, or a backslash. The positions are where they disagree. Step through how JavaScript and three other engines read the same five characters, []a]+:

Interactive · step by step Two readings of the pattern []a]+ Step through how JavaScript reads the five characters, then how Python, PCRE2 and Go read them, and what each finds in x]a].
Two readings of the pattern []a]+, step 7 of 7. Showing JavaScript, [] is empty, a is a literal, ] is a literal, + repeats the ], x]a]: no match, Three engines, []a] is a class, + repeats it, x]a]: ]a] at 1. In focus: x]a]: ]a] at 1.JavaScriptV8 and QuickJS[] is emptymatches nothinga is a literal] is a literal+ repeats the ]x]a]: no matchThree enginesPython, PCRE2, Go[]a] is a classmembers ] and a+ repeats itx]a]: ]a] at 1

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The same five characters. JavaScript and three other engines get the same pattern, []a]+, and read it from left to right. In all of them the opening bracket starts a character class. What happens at the next character, a closing bracket, splits them in two.
  2. JavaScript closes at once. JavaScript ends a class at the first ] it meets, even straight after the opening bracket. So [] is a class with nothing in it, which ECMAScript defines as the empty set: it matches no character at all.
  3. The rest is plain text. Everything after the empty class is outside any class: a is a literal a, the second ] is a literal ], and + repeats that ]. JavaScript reads the pattern as no character, then a, then one or more ].
  4. JavaScript never matches. A pattern that must first match a character from an empty set can never match, whatever the text. On x]a] JavaScript reports no match, and it gives no error or warning.
  5. A first ] is a member. Python, PCRE2 and Go take a ] straight after the opening bracket as a member of the class, as POSIX does. The class goes on to the next ], so it holds two characters: ] and a.
  6. + repeats the class. Here + follows the whole class, so the pattern means one or more characters in a row, each of them a ] or an a.
  7. Three engines find ]a]. On x]a] the search finds no match at the x, then takes ], a and ] in a row from position 1: the match is ]a]. Written as [\]a]+, with the bracket escaped, the pattern finds ]a] in all four engines.

Python, PCRE2 and Go follow the POSIX rule that a ] right after the opening bracket is a member. JavaScript ends a class at the first ] wherever it stands, and ECMAScript defines the empty class [] as a set with no characters, which never matches. The same six probes ran in all four engines; the first three rows are about placement:

Six class probes in four engines

JavaScript (Node 24.21.0) · class-traps/class-traps.mjs

// Six class probes, the same in every engine of this lesson: JavaScript (V8) here.
const probes = [
  ['[]a]+', 'x]a]'],
  ['[\\]a]+', 'x]a]'],
  ['[+^-]', '2+3^4-1'],
  ['[A-z]+', 'Hello_[World]'],
  ['[[:alpha:]]+', 'ab1 cd'],
  ['a[^x]b', 'a\nb'],
];
for (const [pattern, text] of probes) {
  let result;
  try {
    const found = text.match(new RegExp(pattern, 'g'));
    result = found ? JSON.stringify(found) : 'no match';
  } catch (e) {
    result = `${e.name}: ${e.message}`;
  }
  console.log(`${pattern.padEnd(13)} on ${JSON.stringify(text).padEnd(16)} -> ${result}`);
}

Output

[]a]+         on "x]a]"           -> no match
[\]a]+        on "x]a]"           -> ["]a]"]
[+^-]         on "2+3^4-1"        -> ["+","^","-"]
[A-z]+        on "Hello_[World]"  -> ["Hello_[World]"]
[[:alpha:]]+  on "ab1 cd"         -> no match
a[^x]b        on "a\nb"           -> ["a\nb"]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node class-traps.mjs

Python 3.14.8 · class-traps/class_traps.py

"""Six class probes, the same in every engine of this lesson: Python's re here.

re raises FutureWarning for a class that may change meaning in a later version; the warnings module catches it,
so the message prints beside its probe.
"""

import json
import re
import warnings

PROBES = [
    (r"[]a]+", "x]a]"),
    (r"[\]a]+", "x]a]"),
    (r"[+^-]", "2+3^4-1"),
    (r"[A-z]+", "Hello_[World]"),
    (r"[[:alpha:]]+", "ab1 cd"),
    (r"a[^x]b", "a\nb"),
]

for pattern, text in PROBES:
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        found = re.findall(pattern, text)
    result = json.dumps(found, ensure_ascii=False, separators=(",", ":")) if found else "no match"
    print(f"{pattern:<13} on {json.dumps(text):<16} -> {result}")
    for warning in caught:
        print(f"{'':<13}    {warning.category.__name__}: {warning.message}")

Output

[]a]+         on "x]a]"           -> ["]a]"]
[\]a]+        on "x]a]"           -> ["]a]"]
[+^-]         on "2+3^4-1"        -> ["+","^","-"]
[A-z]+        on "Hello_[World]"  -> ["Hello_[World]"]
[[:alpha:]]+  on "ab1 cd"         -> no match
                 FutureWarning: Possible nested set at position 1
a[^x]b        on "a\nb"           -> ["a\nb"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 class_traps.py

Go 1.27.1 · class-traps/class_traps.go

// Six class probes, the same in every engine of this lesson: Go's regexp (RE2 syntax) here.
// The last line runs the newline probe again with CompilePOSIX, whose syntax flags leave out ClassNL.
package main

import (
	"encoding/json"
	"fmt"
	"regexp"
)

func show(pattern, text string, compile func(string) (*regexp.Regexp, error)) {
	quoted, _ := json.Marshal(text)
	result := "no match"
	re, err := compile(pattern)
	if err != nil {
		result = err.Error()
	} else if found := re.FindAllString(text, -1); found != nil {
		list, _ := json.Marshal(found)
		result = string(list)
	}
	fmt.Printf("%-13s on %-16s -> %s\n", pattern, quoted, result)
}

func main() {
	probes := [][2]string{
		{`[]a]+`, "x]a]"},
		{`[\]a]+`, "x]a]"},
		{`[+^-]`, "2+3^4-1"},
		{`[A-z]+`, "Hello_[World]"},
		{`[[:alpha:]]+`, "ab1 cd"},
		{`a[^x]b`, "a\nb"},
	}
	for _, p := range probes {
		show(p[0], p[1], regexp.Compile)
	}
	fmt.Println("The newline probe again, compiled with CompilePOSIX:")
	show(`a[^x]b`, "a\nb", regexp.CompilePOSIX)
}

Output

[]a]+         on "x]a]"           -> ["]a]"]
[\]a]+        on "x]a]"           -> ["]a]"]
[+^-]         on "2+3^4-1"        -> ["+","^","-"]
[A-z]+        on "Hello_[World]"  -> ["Hello_[World]"]
[[:alpha:]]+  on "ab1 cd"         -> ["ab","cd"]
a[^x]b        on "a\nb"           -> ["a\nb"]
The newline probe again, compiled with CompilePOSIX:
a[^x]b        on "a\nb"           -> no match

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run class_traps.go

PCRE2 10.49 · class-traps/class_traps_pcre2.py

"""Six class probes, the same in every engine of this lesson: PCRE2 10.49 here, through its test program pcre2test.

Each pattern line ends in /g (find every match); the indented line under it is the text, and each match prints as
" 0: <text>". In a text line, \\n stands for a newline, and pcre2test shows a matched newline as \\x0a.
"""

from pcre2_session import run

run(r'''
/[]a]+/g
    x]a]

/[\]a]+/g
    x]a]

/[+^-]/g
    2+3^4-1

/[A-z]+/g
    Hello_[World]

/[[:alpha:]]+/g
    ab1 cd

/a[^x]b/g
    a\nb
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/[]a]+/g
    x]a]
 0: ]a]

/[\]a]+/g
    x]a]
 0: ]a]

/[+^-]/g
    2+3^4-1
 0: +
 0: ^
 0: -

/[A-z]+/g
    Hello_[World]
 0: Hello_[World]

/[[:alpha:]]+/g
    ab1 cd
 0: ab
 0: cd

/a[^x]b/g
    a\nb
 0: a\x0ab

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 class_traps_pcre2.py

Row one is the split from the figure: no match in JavaScript, ]a] in the other three. Row two escapes the bracket, and all four agree. Row three, [+^-], works everywhere because the caret is not first and the hyphen is last. Row four repeats the range rule of the last section in all four engines: [A-z]+ took the underscore and both brackets of Hello_[World] into a single match. The position rules of row three hold until a pattern is edited: a member added after the hyphen turns it into a range, and a caret moved to the front negates the class. The table sums up how the two camps read each case:

Inside a class JavaScript Python, PCRE2 and Go
] right after [ ends the class a member
- first or last a member (an error with the v flag) a member
^ anywhere but first a member a member
\], \- or \^ a member a member

The last row is the one to remember: escape all three, [\]\-\^], and every engine of this lesson reads the class the same way, JavaScript with the v flag included. An escaped caret that is not first was never needed, but it costs nothing and keeps its meaning when someone later moves it to the front. One exception lies outside these engines. POSIX tools such as grep read a backslash inside brackets as a plain backslash, so there the standard’s rule applies: put ] first and - last. A later module on tools covers grep.

Checkpoint

Check: Placing ], - and ^ in a class

  1. One question You need one class that matches a closing bracket, a hyphen or a caret, written so that JavaScript, Python, PCRE2 and Go all read it the same way. Which do you write?

    Choose one answer.

    Show the answer: One question

    Answer: [\]\-\^]

    The flavours disagree about a bare ] at the start, and they agree about escapes. With every special character escaped the class has one meaning in all four engines, and v mode in JavaScript accepts it too.

POSIX names work in PCRE2 and Go, and fail silently in JavaScript

Row five of the same probes, [[:alpha:]]+ on the text ab1 cd, found ab and cd in PCRE2 and Go and nothing at all in JavaScript and Python. The double brackets are a POSIX bracket expression: The POSIX name for a [...] class. It may hold class names such as [:alpha:] or [:digit:], as in [[:alpha:]]; PCRE2, Go and grep support those names, while JavaScript and Python read them as ordinary characters.: POSIX defines twelve class names that may stand inside a bracket expression: alnum, alpha, blank, cntrl, digit, graph, lower, print, punct, space, upper and xdigit, each written between [: and :]. The outer brackets are the class and the name is one member of it, so [[:alpha:][:digit:]] is a letter or a digit.

PCRE2 supports the names and gives them ASCII meanings unless Unicode properties are switched on (its UCP option), and Go lists [[:alpha:]] as an ASCII class equal to [A-Za-z]. JavaScript and Python’s re have no POSIX names. They read [[:alpha:] as an ordinary class of the characters [, :, a, l, p and h, and the last ] as a literal, so a match needs one of those characters followed by a ]. Python at least says something: FutureWarning, Possible nested set, because it reserves a class that starts with [ for nested classes in a later version.

What JavaScript makes of the same five characters depends on the flags. Run it with none, with u and with v:

[[:alpha:]] in JavaScript, with and without flags JavaScript · posix-js/posix-js.mjs
// JavaScript has no POSIX names inside brackets. What it makes of [[:alpha:]] depends on the flags.
function show(label, source, flags, text) {
  let result;
  try {
    const found = text.match(new RegExp(source, flags + 'g'));
    result = found ? JSON.stringify(found) : 'no match';
  } catch (e) {
    result = e.name; // the wording of the message differs between engines
  }
  console.log(`${label.padEnd(8)} /${source}/${flags} on ${JSON.stringify(text)}: ${result}`);
}

show('no flag', '[[:alpha:]]+', '', 'ab1 cd');
show('no flag', '[[:alpha:]]+', '', 'a] :]');
show('u flag', '[[:alpha:]]+', 'u', 'ab1 cd');
show('v flag', '[[:alpha:]]+', 'v', 'ab1 cd');
show('v flag', '[[:alpha:]]+', 'v', 'alpha: ph');
show('ranges', '[A-Za-z]+', '', 'ab1 cd');

Output

no flag  /[[:alpha:]]+/ on "ab1 cd": no match
no flag  /[[:alpha:]]+/ on "a] :]": ["a]",":]"]
u flag   /[[:alpha:]]+/u on "ab1 cd": SyntaxError
v flag   /[[:alpha:]]+/v on "ab1 cd": ["a"]
v flag   /[[:alpha:]]+/v on "alpha: ph": ["alpha:","ph"]
ranges   /[A-Za-z]+/ on "ab1 cd": ["ab","cd"]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node posix-js.mjs

Without flags the pattern quietly misses ab1 cd and matches the odd text a] :]. The u flag turns the lone ] into a SyntaxError, which at least fails at once. The v flag accepts nested classes, so [:alpha:] becomes a class of five characters and the pattern finds alpha: in the text alpha: ph. None of the three means “a letter”. To port a POSIX name, write the set it stood for: [A-Za-z] for the ASCII meaning, or a property such as \p{L} when every script’s letters should count.

Go's POSIX mode changes classes too

The last line of the Go tab compiled a[^x]b with regexp.CompilePOSIX, and the class no longer matched the line break. Go’s documentation lists the flag ClassNL, which lets classes such as [^a-z] match a newline, among the flags of its normal syntax and not of POSIX syntax.

Checkpoint

Check: POSIX bracket expressions

  1. Question 1 of 2 In JavaScript without flags, what does /[[:digit:]]/g find in the text Room 7?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing

    Without flags JavaScript closes the class at the first ], so [[:digit:] is a set of six characters and the last ] is a literal. A match would need one of those six followed by ], which Room 7 does not contain, so the result is no match and no error.

  2. Question 2 of 2 In which engines of this lesson does [[:alpha:]]+ find ab and cd in the text ab1 cd?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • PCRE2
    • Go

    POSIX names are part of POSIX bracket expressions, which PCRE2 and Go (RE2) support and JavaScript and Python's re do not. The two that lack them do not refuse the pattern: JavaScript stays silent and Python warns.

Set operations build one class from others

The lowercase consonants are a to z without five letters, and with ranges alone they become [b-df-hj-np-tv-z], five ranges that are hard to read and easy to get wrong. Unicode’s regular expression standard, UTS #18, asks engines for union, intersection and set difference inside a class. A class set operation: Building a character class from other classes by difference, intersection or union, such as [[a-z]--[aeiou]] with JavaScript's v flag: a to z without the vowels. Each flavour that has set operations spells them its own way. builds the consonants from two simpler sets.

JavaScript gained them with the v flag in ECMAScript 2024: [[a-z]--[aeiou]] takes the vowels away from a to z, and [[a-z]&&[^aeiou]] keeps what both sets share, which is the same set. Each operand is a whole class, a single character or a property such as the Unicode property escape: A pattern escape such as \p{L} (any letter) or \p{Nd} (any decimal digit) that matches characters by a Unicode property instead of a list; \P matches the rest. JavaScript needs the u or v flag for it, and Python's re module lacks it. More in “Shorthand classes for digits, words and spaces” \p{L}, any letter. Nesting alone is union: [[a-z][0-9]] with v is a letter from a to z or a digit. Press Edit, take y away as well by writing [[a-z]--[aeiouy]], and run it again:

Difference and intersection with the v flag JavaScript · consonants-v/consonants-v.mjs
// Set operations inside a class need the v flag (ES2024).
const text = 'regular expression x--a]';

// Difference: the letters a to z, minus the five vowels.
console.log('difference  ', JSON.stringify(text.match(/[[a-z]--[aeiou]]+/gv)));
// Intersection: the letters a to z that are also not vowels (the same set).
console.log('intersection', JSON.stringify(text.match(/[[a-z]&&[^aeiou]]+/gv)));
// Any letter of any script except a to z.
console.log('letters     ', JSON.stringify('Hello, Wörld'.match(/[\p{L}--[a-z]]/gv)));
// Without v the same characters are an ordinary class, [[a-z], then --[aeiou]] as text.
console.log('no v flag   ', JSON.stringify(text.match(/[[a-z]--[aeiou]]/g)));

// v mode also wants ( ) [ ] { } / - | escaped inside a class.
for (const source of ['[(]', '[\\(]', '[+-]', '[+\\-]']) {
  try {
    new RegExp(source, 'v');
    console.log(`/${source}/v compiles`);
  } catch (e) {
    console.log(`/${source}/v ${e.name}`);
  }
}

Output

difference   ["r","g","l","r","xpr","ss","n","x"]
intersection ["r","g","l","r","xpr","ss","n","x"]
letters      ["H","W","ö"]
no v flag    ["x--a]"]
/[(]/v SyntaxError
/[\(]/v compiles
/[+-]/v SyntaxError
/[+\-]/v compiles

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node consonants-v.mjs

Without the v flag the same characters are an ordinary class followed by text, which matched x--a]. The v flag also asks for more escapes inside a class: the proposal that added it lists (, ), [, {, }, /, - and | as characters that must be escaped there, besides \ and ], so /[(]/v and /[+-]/v are SyntaxErrors and [+\-] compiles. The flavours that have set operations spell them differently:

Flavour One lowercase consonant
JavaScript, v flag [[a-z]--[aeiou]] or [[a-z]&&[^aeiou]]
PCRE2 10.45 or later (?[ [a-z] - [aeiou] ]), or [[a-z]--[aeiou]] with the option PCRE2_ALT_EXTENDED_CLASS
Java [a-z&&[^aeiou]], from its Pattern documentation
.NET [a-z-[aeiou]], from its documentation of character class subtraction
Python, Go, PHP 8.5.11 none: write the ranges, [b-df-hj-np-tv-z]

The JavaScript, PCRE2, PHP, Python and Go rows ran; the tabs show what each printed for the same text:

The consonants in five flavours

JavaScript (V8) · consonants/consonants.mjs

// Consonants in JavaScript (V8, Node 24): set operations with the v flag, and what v mode refuses.
const text = 'regular expression x--a]';
for (const [source, flags] of [
  ['[[a-z]--[aeiou]]+', 'v'],
  ['[[a-z]--[aeiou]]', ''],
  ['[(]', 'v'],
  ['[a-z--aeiou]', 'v'],
]) {
  try {
    new RegExp(source, flags); // compiled without g first, so an error names the pattern as written
    const found = text.match(new RegExp(source, flags + 'g'));
    console.log(`/${source}/${flags} -> ${JSON.stringify(found)}`);
  } catch (e) {
    console.log(`/${source}/${flags} -> ${e.name}: ${e.message}`);
  }
}

Output

/[[a-z]--[aeiou]]+/v -> ["r","g","l","r","xpr","ss","n","x"]
/[[a-z]--[aeiou]]/ -> ["x--a]"]
/[(]/v -> SyntaxError: Invalid regular expression: /[(]/v: Invalid character in character class
/[a-z--aeiou]/v -> SyntaxError: Invalid regular expression: /[a-z--aeiou]/v: Invalid set operation in character class

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node consonants.mjs

PCRE2 10.49 · consonants/consonants_pcre2.py

"""Consonants by set operations in PCRE2 10.49, through its test program pcre2test.

(?[ ... ]) is Perl's extended class, in PCRE2 from release 10.45; inside it, - takes one set away from another and
spaces are ignored. The alt_extended_class modifier sets PCRE2_ALT_EXTENDED_CLASS, which gives [...] itself the
UTS #18 operators --, && and ||. Without that option the same [...] is an ordinary class. Each match prints as " 0:".
"""

from pcre2_session import run

run(r'''
/(?[ [a-z] - [aeiou] ])+/g
    regular expression x--a]

/[[a-z]--[aeiou]]+/g,alt_extended_class
    regular expression x--a]

/[[a-z]--[aeiou]]/g
    regular expression x--a]

/(?[ \p{L} - [a-z] ])/g,utf
    Hello, W\x{f6}rld
''')

Output

$ pcre2test -q patterns.txt   (PCRE2 10.49)
/(?[ [a-z] - [aeiou] ])+/g
    regular expression x--a]
 0: r
 0: g
 0: l
 0: r
 0: xpr
 0: ss
 0: n
 0: x

/[[a-z]--[aeiou]]+/g,alt_extended_class
    regular expression x--a]
 0: r
 0: g
 0: l
 0: r
 0: xpr
 0: ss
 0: n
 0: x

/[[a-z]--[aeiou]]/g
    regular expression x--a]
 0: x--a]

/(?[ \p{L} - [a-z] ])/g,utf
    Hello, W\x{f6}rld
 0: H
 0: W
 0: \x{f6}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 consonants_pcre2.py

PHP 8.5.11 (PCRE2 10.44) · consonants/consonants.php

<?php
// Consonants in PHP 8.5, whose preg functions use the PCRE2 library bundled with PHP.
// A pattern that does not compile makes preg_match_all() return false and raise a warning; the handler below prints
// the warning's message, so nothing else (such as the script's path) is printed.
set_error_handler(function (int $level, string $message): bool {
    echo "  warning: $message\n";
    return true;
});

echo 'PCRE2 ', explode(' ', PCRE_VERSION)[0], "\n";
$text = 'regular expression x--a]';
$patterns = ['/(?[ [a-z] - [aeiou] ])+/', '/[[a-z]--[aeiou]]/', '/[b-df-hj-np-tv-z]+/'];
foreach ($patterns as $pattern) {
    $count = preg_match_all($pattern, $text, $found);
    $result = $count === false ? 'false' : json_encode($found[0]);
    echo "$pattern -> $result\n";
}

Output

PCRE2 10.44
  warning: preg_match_all(): Compilation failed: unrecognized character after (? or (?- at offset 2
/(?[ [a-z] - [aeiou] ])+/ -> false
/[[a-z]--[aeiou]]/ -> ["x--a]"]
/[b-df-hj-np-tv-z]+/ -> ["r","g","l","r","xpr","ss","n","x"]

Recorded with PHP 8.5.11 (cli) (NTS) on macOS 26 arm64. To run it yourself: php consonants.php

Python 3.14.8 · consonants/consonants.py

"""Consonants in Python's re, which has no set operations yet.

re reads the set-operation syntax of other flavours (JavaScript's --, Java's &&) as ordinary characters of a class
and raises FutureWarning, because a later version may give it the UTS #18 meaning; the warnings module catches the
warning so its message prints under its pattern.
"""

import json
import re
import warnings

TEXT = "regular expression x--a]"

for pattern in (r"[[a-z]--[aeiou]]", r"[a-z&&[^aeiou]]", r"[b-df-hj-np-tv-z]+"):
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        found = re.findall(pattern, TEXT)
    print(f"{pattern} -> {json.dumps(found, separators=(',', ':'))}")
    for warning in caught:
        print(f"  {warning.category.__name__}: {warning.message}")

Output

[[a-z]--[aeiou]] -> ["x--a]"]
  FutureWarning: Possible nested set at position 1
[a-z&&[^aeiou]] -> ["a]"]
  FutureWarning: Possible set intersection at position 4
[b-df-hj-np-tv-z]+ -> ["r","g","l","r","xpr","ss","n","x"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 consonants.py

Go 1.27.1 · consonants/consonants.go

// Consonants in Go's regexp (RE2 syntax), which has no set operations.
package main

import (
	"encoding/json"
	"fmt"
	"regexp"
)

func main() {
	text := "regular expression x--a]"
	for _, pattern := range []string{`(?[ [a-z] - [aeiou] ])+`, `[[a-z]--[aeiou]]`, `[b-df-hj-np-tv-z]+`} {
		re, err := regexp.Compile(pattern)
		if err != nil {
			fmt.Printf("%s -> %v\n", pattern, err)
			continue
		}
		found, _ := json.Marshal(re.FindAllString(text, -1))
		fmt.Printf("%s -> %s\n", pattern, found)
	}
}

Output

(?[ [a-z] - [aeiou] ])+ -> error parsing regexp: invalid or unsupported Perl syntax: `(?[`
[[a-z]--[aeiou]] -> ["x--a]"]
[b-df-hj-np-tv-z]+ -> ["r","g","l","r","xpr","ss","n","x"]

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run consonants.go

PCRE2 inside PHP is older than pcre2test

PCRE2 gained both forms, Perl’s (?[ ... ]) and the UTS #18 operators of PCRE2_ALT_EXTENDED_CLASS, in release 10.45. PHP 8.5.11 bundles PCRE2 10.44, so its PHP tab refuses the extended class with a compile warning and reads [[a-z]--[aeiou]] as an ordinary class, just as Go does.

Python reads the JavaScript and the Java forms as ordinary classes too, and warns that a later version may give them meaning: Possible nested set, Possible set intersection. Of the five engines that ran, only JavaScript with v and PCRE2 10.49 found the consonants with a set operation; PHP, Python and Go found them only with the ranges written out. Where a flavour has no set operations, those ranges are the dependable fallback: write them, then test the characters at both ends of every range and the ones just outside them.

Key idea

Set operations make a class out of other classes, but each flavour spells them its own way and some have none, so a set operation copied into another flavour fails or silently changes meaning.

Checkpoint

Check: Class set operations

  1. Question 1 of 2 Which pattern matches one lowercase English consonant in JavaScript?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: /[[a-z]--[aeiou]]/v

    Set operations need the v flag and whole operands: [[a-z]--[aeiou]] is the letters a to z minus the five vowels, and [[a-z]&&[^aeiou]] is the same set by intersection. Without v neither operator exists.

  2. Question 2 of 2 A PHP 8.5 script runs preg_match_all() with (?[ [a-z] - [aeiou] ])+, which pcre2test 10.49 compiles. What happens in PHP?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It returns false with a warning, because the PCRE2 inside PHP 8.5.11 is 10.44 and (?[...]) arrived in 10.45

    The same library name can hide two releases. pcre2test 10.49 knows Perl's extended classes; PHP 8.5.11 carries PCRE2 10.44, which does not. Check the version a host language bundles (PHP prints it in PCRE_VERSION) before you rely on a new class feature.

Practise: four classes, two flavours

Each task of the exercise names its flavour, because the two do not offer the same tools: in JavaScript the v flag lets you take digits or vowels away from a range, and in Python you write the ranges out yourself. The starter code holds one common mistake per task, and the sample tests run in your browser:

Exercise · Easy · JavaScript, Python

Four character classes in two flavours

Write four character classes, each matching exactly one character. Solve them in JavaScript (patterns.mjs, a regular expression literal for each name) or in Python (patterns.py, the pattern text for each name, which the tests compile with re). The two flavours differ in what they offer, so each task says what its flavour expects:

  1. HEX_DIGIT: one hexadecimal digit, 0 to 9 or a to f in either case. Both flavours.
  2. SPECIAL: one character that is a closing bracket ], a hyphen -, a caret ^ or a backslash \, and no other character. Both flavours; the JavaScript tests also run it with the v flag added, so write it in a form that v mode accepts too.
  3. CONSONANT: one lowercase English consonant, a letter from a to z other than a, e, i, o and u. JavaScript: use the v flag and take the vowels away from [a-z] with set difference, --. Python: re has no set operations, so write the consonants out as ranges; the tests fail on any warning.
  4. DEVANAGARI: one character of the Devanagari block, U+0900 to U+097F, that is not one of its ten digits, U+0966 to U+096F. JavaScript: the v flag and set difference again. Python: ranges.

The starter code holds a common mistake for every task. The sample tests list the characters each class must match and must not match.

JavaScript · Starter code · patterns.mjs

// Each name holds a character class that matches exactly one character. Each one below has a common mistake.

/** One hexadecimal digit, 0-9 or a-f in either case. */
export const HEX_DIGIT = /[0-9A-f]/;

/** One of ] - ^ \ and nothing else (the tests also try it with the v flag). */
export const SPECIAL = /[]-^\\]/;

/** One lowercase consonant, with the v flag and a set operation. */
export const CONSONANT = /[a-z]/v;

/** One Devanagari character (U+0900 to U+097F) that is not a Devanagari digit (U+0966 to U+096F). */
export const DEVANAGARI = /[\u{900}-\u{97F}]/v;
The sample tests · patterns.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { CONSONANT, DEVANAGARI, HEX_DIGIT, SPECIAL } from './patterns.mjs';

// A copy without g or y, so that test() never carries a position over from one call to the next.
const fresh = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ''));
// The same class with the v flag (in place of u, which cannot be combined with v).
const withV = (re) => new RegExp(re.source, re.flags.replace(/[gyuv]/g, '') + 'v');

function check(re, yes, no) {
  const r = fresh(re);
  for (const ch of yes) assert.ok(r.test(ch), `${String(re)} should match ${JSON.stringify(ch)}`);
  for (const ch of no) assert.ok(!r.test(ch), `${String(re)} should not match ${JSON.stringify(ch)}`);
  assert.equal((yes[0] + yes[1]).match(r)[0], yes[0], `${String(re)} should match one character at a time`);
}

test('HEX_DIGIT: one hex digit in either case', () => {
  check(HEX_DIGIT, ['0', '9', 'a', 'f', 'A', 'F'], ['g', 'G', 'Z', '_', '[', '`', '-', ' ']);
});

test('SPECIAL: one of ] - ^ \\ and nothing else, also with the v flag', () => {
  const yes = [']', '-', '^', '\\'];
  const no = ['[', 'a', '_', '`', '.', '"'];
  check(SPECIAL, yes, no);
  let inV = null;
  try {
    inV = withV(SPECIAL);
  } catch {
    // v mode refused the class
  }
  assert.ok(inV, 'the class should also compile with the v flag: escape every character v mode reserves');
  check(inV, yes, no);
});

test('CONSONANT: a to z without the five vowels, by set difference in v mode', () => {
  assert.ok(CONSONANT.flags.includes('v'), 'use the v flag');
  assert.ok(CONSONANT.source.includes('--'), 'take the vowels away with --');
  check(CONSONANT, ['b', 'c', 'm', 'x', 'y', 'z'], ['a', 'e', 'i', 'o', 'u', 'B', '-', '[']);
});

test('DEVANAGARI: the block without its ten digits, by set difference in v mode', () => {
  assert.ok(DEVANAGARI.flags.includes('v'), 'use the v flag');
  assert.ok(DEVANAGARI.source.includes('--'), 'take the digits away with --');
  check(DEVANAGARI, ['क', 'ि', '।', 'ॐ', '\u0900', '\u097F'], ['०', '९', '५', '9', 'a', '\u0A05']);
});

Python · Starter code · patterns.py

"""Each name holds the text of a pattern: a character class that matches exactly one character.

The tests compile each one with re and fail on any warning. Each one below has a common mistake.
"""

# One hexadecimal digit, 0-9 or a-f in either case.
HEX_DIGIT = r"[0-9A-f]"

# One of ] - ^ \ and nothing else.
SPECIAL = r"[]-^\\]"

# One lowercase consonant. re has no set operations: write the consonants as ranges.
CONSONANT = r"[[a-z]--[aeiou]]"

# One Devanagari character (U+0900 to U+097F) that is not a Devanagari digit (U+0966 to U+096F).
DEVANAGARI = r"[\u0900-\u097F]"
The sample tests · test_patterns.py
import re
import warnings

from patterns import CONSONANT, DEVANAGARI, HEX_DIGIT, SPECIAL


def compiled(pattern):
    """Compiles the pattern with every warning turned into an error, so a FutureWarning fails the test."""
    with warnings.catch_warnings():
        warnings.simplefilter("error")
        return re.compile(pattern)


def check(pattern, yes, no):
    r = compiled(pattern)
    for ch in yes:
        assert r.search(ch), f"{pattern} should match {ch!r}"
    for ch in no:
        assert not r.search(ch), f"{pattern} should not match {ch!r}"
    assert r.search(yes[0] + yes[1]).group() == yes[0], f"{pattern} should match one character at a time"


def test_hex_digit():
    """HEX_DIGIT: one hex digit in either case"""
    check(HEX_DIGIT, ["0", "9", "a", "f", "A", "F"], ["g", "G", "Z", "_", "[", "`", "-", " "])


def test_special():
    """SPECIAL: one of ] - ^ \\ and nothing else"""
    check(SPECIAL, ["]", "-", "^", "\\"], ["[", "a", "_", "`", ".", '"'])


def test_consonant():
    """CONSONANT: a to z without the five vowels, as ranges"""
    check(CONSONANT, ["b", "c", "m", "x", "y", "z"], ["a", "e", "i", "o", "u", "B", "-", "["])


def test_devanagari():
    """DEVANAGARI: the block without its ten digits, as ranges"""
    check(DEVANAGARI, ["क", "ि", "।", "ॐ", "\u0900", "\u097f"], ["०", "९", "५", "9", "a", "\u0a05"])
A hint

A range runs from one code point to another, so A-f covers far more than the letters you want: write the upper and lower case ranges separately. Inside a class, a backslash makes ], -, ^ and \ itself plain characters in every flavour, and v mode wants that backslash before the hyphen. In v mode, both sides of -- must be whole classes: [[0-9]--[5]] is the digits without 5.

The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Regex Tester Try your classes on text of your own before you run the tests. The tester uses your browser's JavaScript engine, so a class that passes there behaves the same in the exercise's JavaScript tasks.

Then test yourself on the five concepts. The self-check starts where your answers are weakest, explains every answer the moment you choose it, and brings a missed question back once before it ends:

Practice that adapts to you

Practise this lesson

17 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 How many characters of the text does the class [aeiou] match at one position?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: Exactly one

    [aeiou] means one character, and that character must be a, e, i, o or u. The order of the members and their number do not change how much text the class takes.

  2. Question 2 of 17 The first example runs /7[^0-9]/ on the text 4021-7, whose last character is the 7. Why is there no match?

    Choose one answer.

    Show the answer: Question 2 of 17

    Answer: A negated class still needs one character, and after the final 7 there is none

    [^...] is not "anything but", it is "one character that is not one of these". The engine needs a character to test, so a pattern that ends with a negated class cannot match at the very end of the text.

  3. Question 3 of 17 You must match one character that is not a comma, and line breaks in the text should count as ordinary characters. Which do you write?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: [^,]

    A negated class lists only what it excludes, and it never treats line terminators specially. The dot does the reverse: it excludes line terminators and nothing you choose.

  4. Question 4 of 17 Which of these characters does [A-z] match?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: The underscore _

    A range takes every code point from its first character to its last. From A (U+0041) to z (U+007A) that is 58 characters: 52 letters and [ \ ] ^ _ and the backtick.

  5. Question 5 of 17 What do JavaScript, Python, PCRE2 and Go do with the class [a-Z]?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: They refuse it: a (U+0061) comes after Z (U+005A), so the range runs backwards

    A range needs its first code point to be lower than or equal to its last. [a-Z] breaks that rule, so every flavour of the lesson stops with an error before any text is searched.

  6. Question 6 of 17 [0-F] was meant to match a hex digit written in capitals. Which of these characters does it accept although they are not hex digits?

    Choose every answer that is right.

    Show the answer: Question 6 of 17

    Answer:

    • The colon :
    • The at sign @

    From 0 (U+0030) to F (U+0046) lie the ten digits, the seven symbols : ; < = > ? @ and the capitals A to F. The class for capital hex digits is [0-9A-F]: two ranges, each with nothing between its ends that you do not want.

  7. Question 7 of 17 A product code character is one capital letter A to Z or one digit. Which class is right in JavaScript, Python, PCRE2 and Go alike?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: [A-Z0-9]

    Choose ranges whose ends sit right next to the characters you want, and write one range per block. Every flavour reads [A-Z0-9] the same way.

  8. Question 8 of 17 In JavaScript, what is []?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: A class with no members, which can never match

    JavaScript closes a class at the first ], even right after the opening bracket. That is why []a] means an empty class followed by the text a], and why a ] meant as a member must be escaped.

  9. Question 9 of 17 What does the class [a^b] match?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: An a, a caret or a b

    The caret has two meanings at most in a class: first, it negates; elsewhere, it is a plain caret. Escaping it, \^, is never wrong and saves the reader from checking the position.

  10. Question 10 of 17 Which class matches a closing bracket in JavaScript, Python, PCRE2 and Go alike?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: [\]]

    The leading-bracket rule comes from POSIX, and JavaScript does not follow it. The escape is the one form the four regex engines share. POSIX tools such as grep are different again: they read a backslash inside brackets as itself, so there the ] goes first.

  11. Question 11 of 17 Python 3.14 reads [[:alpha:]]+ and finds nothing in ab1 cd. What else does it do?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: It raises FutureWarning: Possible nested set

    Python's re builds the same wrong class as JavaScript, but it warns about it. Turning warnings into errors in tests, as the exercise does, makes such a pattern fail loudly.

  12. Question 12 of 17 With the v flag, JavaScript accepts /[[:alpha:]]+/v. What does it find in the text alpha: ph?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: alpha: and ph

    The same five characters give three outcomes in JavaScript: no match without flags, a SyntaxError with u, and a class of five characters with v. None of them is "one letter".

  13. Question 13 of 17 A PCRE2 pattern uses [[:upper:]] for one ASCII capital letter. What do you write in JavaScript so it means the same?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: [A-Z]

    Port a POSIX name by writing the set it stands for in the source flavour. PCRE2 without UCP gives the ASCII meaning, so the JavaScript form is a plain range; the Unicode form is a different, larger set.

  14. Question 14 of 17 Which flavour writes "a to z except the vowels" as [a-z-[aeiou]]?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: .NET

    Every flavour that has set operations spells them its own way. Copying one flavour's form into another gives an error at best and a silently wrong class at worst.

  15. Question 15 of 17 The Python tab runs [a-z&&[^aeiou]] on the text regular expression x--a]. Why is its only match a]?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: Python has no set operations: it reads one ordinary class up to the first ], then a literal ]

    Python's re reserves the set-operation syntax for the future and warns, but until then it reads the characters as ordinary class members. A ported set operation therefore runs without an error and matches the wrong things.

  16. Question 16 of 17 Which of these classes does JavaScript accept with the v flag?

    Choose every answer that is right.

    Show the answer: Question 16 of 17

    Answer:

    • [\(\)]
    • [+\-]

    v mode keeps more characters for its own syntax: ( ) [ ] { } / - \ and |, and doubled punctuation such as && or !!, must be escaped inside a class. The same classes are fine without flags and with u.

  17. Question 17 of 17 A JavaScript form must accept one letter of any script except the ASCII letters A to Z and a to z. Which pattern do you write?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: /[\p{L}--[A-Za-z]]/v

    Difference keeps what is in the first set and not in the second. With the v flag, \p{L} supplies the letters of every script and the nested class removes the ASCII ones.

Interview questions

Warm-up: what does a negated class such as [^0-9] match, and how is it different from the dot?

A negated class matches exactly one character that is not in its set: [^0-9] takes any single character except the ten ASCII digits, so it finds the hyphen in 4021-7. Two details separate it from the dot. The set is yours: the dot leaves out only line terminators, while [^0-9] leaves out what you list and nothing else. And a class never treats line terminators specially, so [^0-9] matches a newline that the dot, by default, does not. What it never matches is nothing at all: it must take one character, so 7[^0-9] fails at the end of 4021-7, where no character follows the 7. Go’s CompilePOSIX is an exception to the newline rule, because its classes leave the newline out.

The two questions below ask what happens when a class leaves the flavour it was written for: a POSIX name that moves to JavaScript, and a range that was meant to cover accented letters. Work out where each pattern goes wrong before you open its first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A signup service in PHP checks usernames with preg_match('/^[[:alnum:]_-]+$/u', $name). Moved to Node.js as /^[[:alnum:]_-]+$/, it now rejects every username, with no error. Explain what JavaScript does with the pattern, and how you would port the check so both services accept the same names.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A registration form checks names with ^[A-Za-zÀ-ÿ]+$, meant as "letters, accents allowed". Testers report that A×B is accepted, and that Łukasz and नेहा are rejected. Explain all three results and propose a class that does what the form meant.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How many characters does a character class such as [0-9A-Fa-f] match at a time?

    Exactly one, at one position: any one of its members. A six-digit hex colour such as C0FFEE therefore needs the class six times.

  2. Why does the class [A-z] match 58 characters instead of 52 letters?

    A range takes every code point from its first character to its last, and six symbols sit between Z and a. Give each block its own range, [A-Za-z]. A backwards range such as [a-Z] is an error.

  3. What can the negated class [^0-9] match that the dot does not match by default?

    A line break: a class never treats line terminators specially, so [^0-9] takes a newline. It still needs one character, so 7[^0-9] fails at the very end of the text.

  4. How do you put ], - and ^ inside one class so that every flavour reads them as members?

    Escape all three: [\]\-\^]. JavaScript ends a class at the first ] and reads [] as a class that never matches, while Python, PCRE2 and Go take a leading ] as a member.

  5. Where can you use a POSIX name such as [[:alpha:]]?

    In PCRE2 and Go. JavaScript turns it into a wrong class without an error, and Python builds the same wrong class with only a FutureWarning.

  6. How is the set of letters a to z without the vowels written as one class?

    [[a-z]--[aeiou]] in JavaScript with the v flag, (?[ ... ]) in PCRE2 from 10.45, && in Java and a subtraction -[...] in .NET. In Python, Go and PHP 8.5.11 you write the remaining ranges out by hand.

Key takeaways

  • A character class matches exactly one character at one position, any one of its members; a caret right after the opening bracket negates it, and a negated class still takes one character, line breaks included, unlike the dot.
  • A range covers every code point from its first character to its last, so [A-z] takes 58 characters instead of 52, and a backwards range such as [a-Z] is an error in every flavour of this lesson.
  • JavaScript ends a class at the first ] and reads [] as a class that never matches, while Python, PCRE2 and Go take a first ] as a member, so the portable class escapes all three special characters: [\]\-\^].
  • POSIX names such as [[:alpha:]] work in PCRE2 and Go; JavaScript turns them into a wrong class without any error, and Python into the same wrong class with a FutureWarning.
  • Set operations build a class from others, written [[a-z]--[aeiou]] in JavaScript with the v flag, (?[ ... ]) in PCRE2 from 10.45, && in Java and -[...] in .NET, while Python, Go and PHP 8.5.11 need the ranges written out.

Terms in this lesson

character class
A part of a pattern between square brackets that matches exactly one character of the text: any one of the characters, ranges and classes listed inside it, such as [aeiou] for one vowel.
character range
Two characters joined by a hyphen inside a character class, such as a-z. It matches every character whose code point lies between the two, both included, so A-z also takes the six symbols between Z and a.
class set operation
Building a character class from other classes by difference, intersection or union, such as [[a-z]--[aeiou]] with JavaScript's v flag: a to z without the vowels. Each flavour that has set operations spells them its own way.
negated class
A character class that starts with a caret right after the opening bracket, such as [^0-9]. It matches exactly one character that is not in the set, line breaks included.
POSIX bracket expression
The POSIX name for a [...] class. It may hold class names such as [:alpha:] or [:digit:], as in [[:alpha:]]; PCRE2, Go and grep support those names, while JavaScript and Python read them as ordinary characters.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.