Skip to content

[...] — a set of characters, and the two whose position matters

One line: Brackets match one character from a set — ranges with -, negation with a leading ^, escapes work inside — and the portable rules are that ] is literal only when it comes first and - only when it comes last; beyond that, Ruby, Java and Rust do set arithmetic inside the brackets and everybody else reads the same characters as literals.

Level: reference · one construct, eight engines

What it means

A class is the one place in a pattern where most metacharacters go quiet: ., *, +, ?, (, | are all themselves inside [...]. Four characters keep a job:

Character Its job inside [...] When it is literal instead
\ escape never — write \\
^ negate, if first anywhere but first
- range, between two characters first, last, or escaped
] close first (most engines), or escaped

And in three engines there is a fifth: && for intersection, and nested [...] for union — Java's and Ruby's syntax, adopted by Rust's regex.

Measured

Verified output of kw_character_class_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

construct                 python  perl   node   ruby   java  go    rust  fancy
range                     bca     bca    bca    bca    bca   bca   bca   bca
negated                   x       x      x      x      x     x     x     x
] first is literal        ]       ]      no     ]      ]     ]     ]     ]
- last is literal         -       -      -      -      -     -     -     -
escape inside             3.14    3.14   3.14   3.14   3.14  3.14  3.14  3.14
backslash-b is backspace  \u{8}   \u{8}  \u{8}  \u{8}  -     -     -     \u{8}
intersection &&           no      no     no     b      b     no    b     b
nested class              no      no     no     b      b     no    b     b
empty class               -       -      no     -      -     -     -     -

Reading the table

  • Rows 1–2, 4–5 are the portable core, and all eight agree.
  • Row 3, []a]. A ] in first position is literal in seven engines and the class is "] or a". JavaScript reads [] as an empty class — a set with nothing in it, which can never match — followed by the literal a]; the row's no is the pattern failing to match ]. The portable spelling is [\]a].
  • Row 6, [\b]. Inside brackets \b is a backspace (U+0008) in Python, Perl, JavaScript, Ruby and fancy-regex, and refused by Java, Go and Rust's regex. It is a word boundary in none of them — a boundary is a position, and a class holds characters.
  • Row 7, [a-z&&[^aeiou]]. Ruby, Java, Rust and fancy-regex compute the intersection — lowercase letters that are not vowels — and match b. Python, Perl, JavaScript and Go read &, [ and ^ as five more characters in the set, then need a literal ] after it; the row's no is that pattern failing on aeb. Same bytes, two syntaxes, no error from either side. Row 8, the nested class, splits the same way.
  • Row 9, a[]b. An empty class is a compile error in seven engines and legal in JavaScript — where it is a set that matches nothing, so a[]b can never match. JavaScript's answer is consistent with row 3; it is also the one that surprises everyone who arrives from anywhere else.

The trap

Set operations. A pattern written for Java or Rust with && in a class ports to Python without complaint and matches a different set. Nothing about [a-z&&[^aeiou]] looks wrong in a Python code review, and Python's answer is not "syntax error" but "a class containing &". If a class has to work in more than one engine, spell the intersection out — [b-df-hj-np-tv-z] — or subtract with a lookahead, (?![aeiou])[a-z], where lookahead exists.

Where it is measured in depth

See also