Skip to content

\X — one character as a person counts it

One line: \X matches an extended grapheme cluster — a base character with its combining marks, an emoji with its skin tone and joiners, a flag made of two regional indicators — in Perl, PCRE2, Ruby and Java; . matches one code point everywhere and so splits all three; and Python, Go and Rust have no \X, while JavaScript reads it as the letter X.

Level: reference · one construct, eight engines

What it means

é can be one code point (U+00E9) or two (e U+0065 followed by U+0301 COMBINING ACUTE ACCENT). A family emoji is up to seven code points glued with U+200D ZERO WIDTH JOINER. A flag is two regional-indicator symbols. To a person each of those is one character; to . each is one code point, and . on the second é matches the e and leaves the accent behind.

\X is the escape that matches the whole cluster, using the boundary rules in UAX #29 ↗.

Measured

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

construct                  python  perl                        node  ruby                        java                        go  rust  fancy
. on e + combining accent  e       e                           e     e                           e                           e   e     e
\X on e + accent           -       e\u{301}                    no    e\u{301}                    e\u{301}                    -   -     -
\X on a ZWJ sequence       -       \u{1F468}\u{200D}\u{1F469}  no    \u{1F468}\u{200D}\u{1F469}  \u{1F468}\u{200D}\u{1F469}  -   -     -
\X on a flag pair          -       \u{1F1EB}\u{1F1F7}          no    \u{1F1EB}\u{1F1F7}          \u{1F1EB}\u{1F1F7}          -   -     -
\X in JavaScript           -       X                           X     X                           X                           -   -     -

Reading the table

  • Row 1. All eight engines match e alone — . stops before the combining accent. This is not an engine defect; it is what . is defined to do.
  • Row 2, \X on the same subject. Perl, Ruby and Java take both code points as one match. Python, Go and Rust's two crates refuse the escape. JavaScript's no is the identity escape: \X is X, and there is no X in the subject — row 5 confirms it by matching one.
  • Rows 3–4. The ZWJ family and the flag pair come back whole from the same three engines. Java's \X has understood emoji sequences since Java 9; older Java matched only base-plus-marks.

The trap

Truncating text with a regex. ^.{10} to take "the first ten characters" of a string can cut a family into a man and a joiner, or a flag into half a flag, in every engine on this page — and in JavaScript without u it can cut a single emoji in half. Where \X exists, ^\X{10} is the honest version; where it does not, the host language has a grapheme iterator (Rust's unicode-segmentation, Go's x/text, Python's third-party regex or grapheme) and the regex is the wrong tool.

Where it is measured in depth

See also