Regex Metacharacters and the $ Newline Trap

Every regex metacharacter with real outputs: Multiline vs Singleline, the $ trailing-newline trap, and which digits \d matches per engine.

Revised November 2026. This tutorial first appeared in Korean in 2021. We re-ran its examples in .NET 10 (System.Text.RegularExpressions), Python 3.9 and 3.12 re, PHP 8.5 (PCRE2 10.47) and Dart 3.11, whose RegExp follows JavaScript syntax. The original table needed several fixes: \d and \w are not [0-9] and [a-zA-Z0-9_] in .NET by default, the \s list left out the space character, and \x09 was introduced as a "newline" when it is a tab. We added ^, $, |, { }, Multiline and Singleline, and the trailing-newline trap.

This article is for anyone who wants every special regex symbol in one place, and for anyone whose ^\d+$ validation let through something it should not have. The meaning of each metacharacter is mostly the same across engines. The edge behavior is not: where $ matches and which digits \d accepts differed between the engines we tested. In input validation, those differences become security bugs.

The 14 metacharacters at a glance

These characters have a special meaning inside a pattern. To match one literally, put a \ in front of it (\., \$, \(). Escaping a whole user-supplied string is covered in simple string matching.

Metacharacter Meaning Details
. Any single character except newline This page
^ $ Start and end of the string (or line) This page
| Alternation (or) This page
? * + 0 or 1, 0 or more, 1 or more Quantifiers
{ } Repetition count: {2}, {2,5} Repeated characters
( ) Group Subexpressions
[ ] Character set: [abc] Character sets
\ Escape, and sequences such as \d This page

| has the lowest precedence of all, which causes a classic mistake. ^cat|dog$ means "^cat or dog$", not "a line that is exactly cat or dog". On three lines, catalog, hotdog and dogma, with Multiline on, all four engines matched the cat in catalog and the dog in hotdog. Group the alternatives, ^(cat|dog)$, and only a line consisting of just dog matched.

A { that does not look like a quantifier was treated as a literal by most engines: x{a} matched the text x{a} in all four. a{,2} split them. .NET and Dart read it as the literal text a{,2}; Python and PHP (PCRE2 10.47) read it as the quantifier {0,2}. Dart in unicode: true mode rejected it with Lone quantifier brackets. When you mean a literal brace, always write \{.

Escape sequences and the 2021 examples

Here is the 2021 table, corrected against the actual results:

Sequence Meaning Correction
\t \n \r \f Tab, line feed, carriage return, form feed
\v Vertical tab (U+000B) In PHP (PCRE) it is any vertical whitespace, so it also matches \n
[\b] Backspace (only inside a set) Outside a set, \b is a word boundary
\xHH One character by hex code (\x09 = tab) Not "matches hex digits"
\0, \012 One character by octal code (\0 = NUL, \012 = \n) Not "matches octal digits"
\d \D Digit / non-digit May include Unicode digits (see below)
\w \W Word character / not .NET includes Hangul and CJK by default
\s \S Whitespace / not The original list was missing the space

\x needs exactly two hex digits. .NET rejected both \x and \x4 with Insufficient or invalid hexadecimal digits, Python with incomplete escape \x4, while Dart in its default mode quietly read \x as the letter x. On the text a\nb\x0Bc, \v found only the \x0B in .NET, Python and Dart, but both \n and \x0B in PHP.

The 2021 examples gave the original results in every engine:

\[\d\]  on [0] [a] [1][2] [b] [c] [3]      -> [0], [1], [2], [3]
\[\W\]  on [0] [-] [a] [B] [$][@] [3]      -> [-], [$], [@]
\x09    on Text1<tab>A Text2<tab>B Text3   -> two tabs

[ and ] build character sets, so they are escaped as \[ and \]. The original introduced \x09 as "the hex value for newline" and then found tabs. 0x09 is tab; line feed is \x0A.

\d, \w and \s differ between engines

The 2021 table said \d equals [0-9]. On the same input, the engines disagreed. The input was ascii 123 / arabic-indic ١٢٣ / fullwidth 123 / devanagari १२:

Engine and mode \d+ matches
.NET 10 default 123, ١٢٣, 123, १२
.NET 10 RegexOptions.ECMAScript 123
Python 3.9 / 3.12 (str pattern) 123, ١٢٣, 123, १२
Python re.ASCII 123
PHP 8.5 /u 123, ١٢٣, 123, १२
PHP 8.5 without /u 123
Dart (default and unicode: true) 123

This bites when validation and parsing disagree. In .NET, 123 passed ^\d+$, and then int.Parse("123") threw FormatException: The input string '123' was not in a correct format. Full-width digits are common with Japanese input methods. Python's int('123'), by contrast, returned 123. If you want ASCII digits, write [0-9] or turn on RegexOptions.ECMAScript (.NET) or re.ASCII (Python). In PHP, adding /u made \d match Unicode digits too.

\w and \s showed the same split:

Input .NET default .NET ECMAScript Python Python re.A PHP /u Dart
\w+ on hello 안녕 日本語 café_1 all 4 words hello, caf, _1 all 4 hello, caf, _1 all 4 hello, caf, _1
\s on space, NBSP, U+3000, tab all 4 space, tab all 4 space, tab all 4 all 4

Note the last row. Dart (JavaScript) treated every Unicode space as \s, but .NET's ECMAScript option matched only the space and the tab. The ECMAScript option is not a perfect copy of JavaScript. Character ranges you write yourself are covered in character sets.

^ and $ with Multiline, . with Singleline

By default, ^ and $ match at the start and end of the whole string; with Multiline they match at the start and end of each line. By default . does not match \n; with Singleline (Python re.S, PHP /s, Dart dotAll) it does. Tested on first line\nsecond line\nthird, with identical results in all four engines:

Pattern No option Multiline Singleline Both
^\w+ first first, second, third first first, second, third
\w+$ third line, line, third third line, line, third
first.*third no match no match whole text whole text

Despite the names, the two options are not opposites. Multiline acts on ^ and $; Singleline acts on .. You can use both together.

The dot had two engine differences. First, .+ on ab\r\ncd returned ab\r, including the carriage return, in .NET, Python and PHP; Dart returned ab. Second, ^.$ on a single emoji 😀 matched in Python and PHP (/u), but not in .NET, where ^..$ matched instead. A .NET . matches one UTF-16 code unit, and the emoji is two. Dart behaved like .NET by default and like Python with unicode: true.

The $ trap: it also matches before a final newline

This is the most common validation bug in this area. ^\d+$ as a "digits only" check also accepts "123\n":

Pattern ^\d+$ on "123\n"
.NET 10     : match ("123")
Python 3.9  : match
Python 3.12 : match
PHP 8.5     : match
Dart        : no match

In .NET, Python and PHP, $ matches at the end of the string or just before a newline at the end. With two newlines, "123\n\n", no engine matched. The trap bites when you validate values that easily carry a trailing \n: form fields, lines read from a file, header values.

Flow chart. Input is 123 followed by a newline, tested with ^\d+$. Decision: which engine? In .NET, Python and PHP the result is a match, because $ also matches before a final newline. In JavaScript and Dart there is no match. The fix: use \z in .NET and PHP, \Z or re.fullmatch in Python, or the D modifier in PHP. All of these reject the input.
Why ^\d+$ accepts "123\n": in .NET, Python and PHP, $ also matches just before a final newline. JavaScript (tested via Dart) does not. Anchor to the true end instead.

How to anchor at the true end differs per engine:

Engine ^\d+\z ^\d+\Z Other options
.NET 10 no match (true end) match (same as $)
PHP 8.5 no match match /^\d+$/D gives no match
Python 3.9 / 3.12 error bad escape \z no match (true end) re.fullmatch(r"\d+", s) returns None
Dart no match no match $ already means the true end

Python's \Z means what \z means in .NET and PHP. The same escape does opposite things in different engines, so check it whenever you port a pattern. Dart with unicode: true rejected both \z and \Z with Invalid identity escape.

Windows line endings add one more trap. On ab\r\ncd\r\n, Multiline ^\w+$ found nothing in .NET, Python and PHP. $ matches before \n, but there is a \r in the way that \w+ cannot consume. Dart treats \r as a line terminator and found ab and cd. PHP found both after we prefixed the pattern with (*ANYCRLF). In .NET, write \r?$ or split the text into lines first.

Summary

Situation Recommendation
Validate an entire value .NET and PHP ^…\z, Python re.fullmatch, Dart ^…$
ASCII digits only [0-9], or .NET ECMAScript / Python re.ASCII
Process each line Multiline, plus \r?$ for CRLF text
.* across lines Singleline (re.S, /s, dotAll)
Literal {, ., $ \{, \., \$
Alternation | Wrap the alternatives in ( )

When to use a regex at all is discussed in What is regex?; repetition continues in quantifiers and repeated characters.

Tested on macOS (Apple Silicon Mac) with .NET 10.0.14, Python 3.9.6 and 3.12.14, PHP 8.5.6 (PCRE2 10.47) and Dart 3.11.5, November 2026.

FAQ

Why does ^\d+$ accept "123\n"?

In .NET, Python and PHP, $ matches at the end of the string and also just before a final newline. To anchor at the true end, use \z in .NET and PHP, \Z or re.fullmatch in Python, or the D modifier in PHP. Dart, which follows JavaScript syntax, did not match in the first place.

Is \d the same as [0-9]?

Not everywhere. In .NET by default, in Python 3 str patterns and in PHP with /u, \d+ matched Arabic-Indic digits ١٢٣, full-width digits 123 and Devanagari digits. With .NET RegexOptions.ECMAScript, Python re.ASCII, or in Dart, only the ASCII 123 matched.

What is the difference between Multiline and Singleline?

Multiline makes ^ and $ match at the start and end of every line. Singleline makes . match the newline \n as well. They affect different metacharacters, so you can turn both on; with both on, both effects applied.

  • #regex
  • #metacharacters
  • #multiline
  • #dotnet
  • #python
  • #php

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑