Skip to content

$ is not the end of the string

One line: In Perl, Python, Java and PCRE2 $ matches at the end of the subject or just before a newline at the end of it, and in Ruby ^ and $ are line anchors always, with no flag involved — so ^\d+$ is a question about a line, not about a value, and a validator built out of it can be walked past.

Level: 301 · you write validators, and somebody else picks the input

Keywords on this page: $ · ^ · \A \z \Z · (?i) — Topics: Validating input · Security · Multiline text and logs

The shortest version

^\d+$ is how nearly everyone spells "this is a number". It is not that check in any of the engines below, and in one of them it is not even close.

Three subjects, printed escaped throughout because two of them contain a newline and a page has to show you where the line ends:

Subject Where it comes from
"1" what the validator's author had in mind
"1\n" the same value read from a file, a socket, a CSV cell, a textarea
"1\nrm -rf /" a second line the validator was never shown, chosen by whoever is sending the input

In the tables below, in the second and third columns, match is the bug.

Perl — where the definition comes from

Perl's perlre is the document every other engine on this page either copied or deliberately declined to copy, and it does not define $ as the end of the string. It defines it as the end of the string or the position before a newline at the end of the string — and that second clause is the whole lesson.

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

perl
pattern                       "1"     "1\n"   "1\nrm -rf /"
/^\d+$/                       match   match   no
/\A\d+\z/                     match   no      no
/\A\d+\Z/                     match   match   no
/^\d+$/m                      match   match   match

/^(\d+)$/ on "1\n" captured '1'; the subject is still 2 chars

flag                          none    /m      /s
^\d+$ on "1\nrm -rf /"        no      match   no
a.b on "a\nb"                 no      no      match

"1\n" passes ^\d+$. The capture is 1, so a program that goes on to use $1 is fine — and a program that goes on to use the original value, which is what validation code normally does, is now holding a string with a newline in it that a regular expression has certified as digits.

"1\nrm -rf /" does not pass, because the newline in it is not at the end. That is the one mercy in the Perl family, and it evaporates the moment someone adds /m: the last row shows the payload sailing through.

\A\d+\z is the row with no surprises in it. It is the only one of the four.

Python — and a \Z that means the opposite

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

python re
pattern                       "1"     "1\n"   "1\nrm -rf /"
search(r"^\d+$")              match   match   no
search(r"\A\d+\Z")            match   no      no
search(r"(?m)^\d+$")          match   match   match
fullmatch(r"\d+")             match   no      no

^\d+$ on "1\n" matched '1' and ended at offset 1 of 2

flag                          off     re.M    re.S
^\d+$ on "1\nrm -rf /"        no      match   no
a.b on "a\nb"                 no      no      match

with re.M, on "rm -rf /\n1":
  ^\d+$                       match
  \A\d+$                      no
  (?-m:^\d+$)                 no

Python's $ behaves exactly like Perl's, which is unsurprising; re is a Perl port. What is not a port is the name:

Python's \Z is Perl's \z. It means the end of the string and nothing else. Perl's own \Z — end of string or before a final newline — has no spelling in Python at all.

The two lines to read side by side are /\A\d+\Z/ in the Perl block above (match on "1\n") and search(r"\A\d+\Z") here (no on "1\n"). Same four characters, opposite answers, no error either way. A pattern moved from Perl to Python gets stricter and a pattern moved from Python to Perl gets looser, silently, in the direction that matters.

Python grew a \z of its own in 3.14, so the collision now has a third state — a spelling that is right on a new interpreter and a hard error on the one your CI is probably running:

Real run — macOS 26.6.2 x86-64, 2026-09-22.

$ python3.12 -c 'import re; re.compile(r"\A\d+\z")' 2>&1 | tail -1
re.error: bad escape \z at position 5
$ python3.14 -c 'import re; re.compile(r"\A\d+\z")' 2>&1 | tail -1
$

Until 3.14 is your floor, write \Z in Python and leave a comment saying it is Perl's \z. (This is also why \z appears nowhere in the generated blocks on this page: the answer would differ between the machine that recorded it and the machine that checks it.)

The last three lines of the block are the other half of the pair. \A is what ^ stops being once re.M is on: put the payload first and the digits last, and ^\d+$ finds the last line while \A\d+$ refuses to look anywhere but the start of the string. (?-m:…) puts the flag back — in the scoped form, because a bare (?-m) is a compile error in Python, and because the same three characters mean something else again in Ruby, two sections down.

And re.fullmatch is the exit from the whole argument. It takes an unanchored pattern and requires it to account for every character, so there is nothing to get wrong.

Java — the same rule, over a bigger alphabet

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

java
pattern                       "1"     "1\n"   "1\nrm -rf /"
find(), ^\d+$                 match   match   no
find(), \A\d+\z               match   no      no
find(), \A\d+\Z               match   match   no
find(), MULTILINE ^\d+$       match   match   match
String.matches("\\d+")        match   no      no

MULTILINE ^\d+$ against one payload per terminator
"1\nrm -rf /"                 match   match (UNIX_LINES)
"1\r\nrm -rf /"               match   no (UNIX_LINES)
"1\u0085rm -rf /"             match   no (UNIX_LINES)
"1\u2028rm -rf /"             match   no (UNIX_LINES)

find(), ^\d+$ on "1\r\n"      match
find(), \A\d+\z on "1\r\n"    no

Two things are Java's own.

First, find() versus matches(). Matcher.find() searches, so ^\d+$ is a question about whether some position exists; String.matches requires the pattern to cover the whole input, exactly like re.fullmatch. If ABAP is in your week too, matches( val = … pcre = … ) is the same idea again.

Second, Java's line terminator is a set, not a character. Pattern counts LF, CR, CRLF, NEL (U+0085), LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) as ending a line, and $ is defined against that set rather than against \n. The middle block measures four of them: with MULTILINE on, a payload separated by any of the four gets a match, and UNIX_LINES is the flag that narrows it back to LF alone. The last two lines show a plain $ — no MULTILINE anywhere — stepping over a final CRLF, which is to say a validator that looked airtight against Unix input is looser still against Windows input.

\Z and \z in Java are Perl's, not Python's: \Z allows the final terminator and \z does not.

Ruby — no flag, and no way to turn it off

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

ruby
pattern                       "1"     "1\n"   "1\nrm -rf /"
/^\d+$/                       match   match   match
/\A\d+\z/                     match   no      no
/\A\d+\Z/                     match   match   no
/(?-m)^\d+$/                  match   match   match

/^\d+$/ matched "1" inside a 10-char subject

flag                          none    /m      (?-m)
^\d+$ on "1\nrm -rf /"        match   match   match
a.b on "a\nb"                 no      match   no

on "rm -rf /\n1":
  /^\d+$/                     match
  /\A\d+$/                    no

This is the row the page exists for.

In Ruby, ^ and $ are line anchors. Always. There is no MULTILINE to have forgotten, because there is no such flag; ^\d+$ asks "is any line of this string all digits?" and the payload answers yes. A Ruby validator that reads input =~ /^\d+$/ accepts "1\nrm -rf /" on the day it is written, with no flag, no option and no warning. The last two lines of the block make the point without the trailing-newline distraction: against "rm -rf /\n1", where the digits are on the second line, ^\d+$ still finds them and \A\d+$ still does not.

Nor can it be switched off. (?-m:…) is how Python cancels its line-anchor flag, and the same letter in Ruby cancels the dot flag: the block shows /(?-m)^\d+$/ leaving ^\d+$ exactly as permissive as it was, while /(?-m)a.b/m quietly stops . from crossing a newline.

That is the flag-letter collision, and it is worth its own table because both halves are the same letter:

Engine Line anchors — ^/$ per line Dot matches newline
Perl /m /s
Python re re.M / (?m) re.S / (?s)
Java Pattern.MULTILINE / (?m) Pattern.DOTALL / (?s)
JavaScript m s
PCRE2 / ABAP PCRE (?m) (?s)
Go regexp (?m) (?s)
Rust regex (?m) (?s)
Ruby always on; no flag exists /m

Every other row spells the dot flag s. Ruby spells it m. So /…/m copied out of a Ruby codebase into any of the others turns on the wrong feature, and /…/m copied into Ruby turns on a feature the reader thought they were already getting.

Ruby's way out is the one it has always documented: \A and \z. \A\d+\z is the only row in the Ruby block that refuses both payloads, and \Z is Perl's \Z — it takes the trailing newline.

JavaScript — right by default, and with no fallback

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

javascript
pattern                       "1"     "1\n"   "1\nrm -rf /"
/^\d+$/                       match   no      no
/^\d+$/m                      match   match   match

/\A\d+\z/ on "A1z"            match
/\A\d+\z/ on "1"              no
new RegExp("\\A\\d+\\z", "u") SyntaxError

flag                          none    m       s
^\d+$ on "1\nrm -rf /"        no      match   no
a.b on "a\nb"                 no      no      match

JavaScript is the engine that means what people think. Its $ is $: the end of the input, no newline clause. /^\d+$/ rejects both payloads — a safe default, shared on this page only with Go and Rust.

It is also the engine with nothing to fall back on. There is no \A and no \z in JavaScript, and in a pattern without the u flag they are not even errors — an unrecognised escape is the escaped character, so /\A\d+\z/ is the letter A, some digits, and the letter z. The middle block measures that: it matches "A1z" and rejects "1". Somebody porting a Ruby validator one line at a time gets a pattern that compiles, runs, and rejects every valid input — fail-closed, this time, but for a reason nobody will find by reading it. Add u and it becomes a SyntaxError, which is the kinder failure.

So the JavaScript rule is the opposite of everyone else's: ^…$ is already correct, and the danger is the m flag — added for one pattern, left on a regex that was later reused for validation.

Go — $ is the end of the haystack

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

go regexp
pattern                       "1"     "1\n"   "1\nrm -rf /"
^\d+$                         match   no      no
\A\d+\z                       match   no      no
\A\d+\Z                       error   error   error
(?m)^\d+$                     match   match   match

\A\d+\Z                       refused at compile time

flag                          none    (?m)    (?s)
^\d+$ on "1\nrm -rf /"        no      match   no
a.b on "a\nb"                 no      no      match

Go's regexp is RE2, and RE2 declined the newline clause: regexp/syntax documents $ by saying it behaves like \z and not like \Z. The first two rows measure exactly that — ^\d+$ and \A\d+\z give the same three answers. \Z is not quietly reinterpreted either; it is refused at compile time, which means a pattern carried over from Perl announces itself instead of changing meaning.

(?m) is still there, and still turns $ into a line anchor that takes the payload. The default is the safe one; the flag is the sharp edge.

Rust — both crates, measured

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

rust regex
pattern                       "1"     "1\n"   "1\nrm -rf /"
^\d+$                         match   no      no
\A\d+\z                       match   no      no
\A\d+\Z                       error   error   error
(?m)^\d+$                     match   match   match

rust fancy-regex
pattern                       "1"     "1\n"   "1\nrm -rf /"
^\d+$                         match   no      no
\A\d+\z                       match   no      no
\A\d+\Z                       match   match   no
(?m)^\d+$                     match   match   match

flag (regex)                  none    (?m)    (?s)
^\d+$ on "1\nrm -rf /"        no      match   no
a.b on "a\nb"                 no      no      match

regex: regex parse error:
    \A\d+\Z
         ^^
error: unrecognized escape sequence

regex agrees with Go, as you would expect from two engines with the same ancestry: $ is the end of the haystack, \A and \z exist, \Z is an unrecognised escape.

fancy-regex is the interesting one, because it is the backtracking crate — the one you reach for when you are porting Perl — and it is not Perl here. It accepts \Z with Perl's meaning ("1\n" matches, the payload does not), and yet its $ stays RE2's: ^\d+$ rejects "1\n". A crate that gives you Perl's \Z and Go's $ is a combination worth knowing about before you port a pattern and assume the semantics came with it.

PCRE2, and therefore ABAP

ABAP's PCRE addition (7.55 and later) is the PCRE2 library, so ABAP's anchors are Perl's anchors. That is checkable on any machine with pcre2grep.

pcre2grep -M hands the engine several lines as one subject — which is what an ABAP string holds — but it also turns PCRE2_MULTILINE on, so the patterns below spell the flag out inline to separate the two questions.

Real run — PCRE2 10.48 (pcre2grep), macOS 26.6.2 x86-64, 2026-09-22. bare.txt holds "1" with no trailing newline; one.txt holds "1\n"; two.txt holds "1\nrm -rf /\n". -c prints the count of matching subjects.

$ pcre2grep -M -c '(?-m)^\d+$' bare.txt
1
$ pcre2grep -M -c '\A\d+\z' bare.txt
1

$ pcre2grep -M -c '(?-m)^\d+$' one.txt
1
$ pcre2grep -M -c '\A\d+\z' one.txt
0
$ pcre2grep -M -c '\A\d+\Z' one.txt
1

$ pcre2grep -M -c '(?-m)^\d+$' two.txt
0
$ pcre2grep -M -c '(?m)^\d+$' two.txt
1
$ pcre2grep -M -c '\A\d+\z' two.txt
0

Line by line: on the honest value both spellings agree. On one.txt, with multiline explicitly off, $ still takes the trailing newline — Perl's clause, in the library ABAP's kernel links against. \Z takes it too, and \z is the one that refuses. And the payload in two.txt is refused by (?-m)^\d+$ and accepted the moment (?m) is on.

The ABAP source is checked by abaplint and not run — nothing in this repository has an ABAP system — so there is no answer key beside it:

z_dollar_end.prog.abap in full — pasted here by tools/run_examples.py from the file CI runs.

REPORT z_dollar_end.

" The anchors in ABAP are PCRE2's, because the PCRE addition (7.55 and later)
" is the PCRE2 library in the kernel: `$` is the end of the string OR the
" position just before a newline at the end of it, `\Z` is the same, and `\z`
" is the end of the string and nothing else.
"
" Nothing in this repository can run ABAP, so this file is source only --
" abaplint parses it and no answer key is recorded beside it. What WAS run is
" the engine underneath; see the dated pcre2grep transcript on the lesson page.

" The value a validator is handed after a flat file, an IDoc segment or a form
" field has kept its line break.
DATA(input) = |1{ cl_abap_char_utilities=>newline }|.

" A SEARCH. FIND asks whether a position exists somewhere in the value, not
" whether the whole value is digits, and `$` will step over that trailing
" newline to find one.
FIND FIRST OCCURRENCE OF PCRE `^\d+$` IN input.
DATA(loose) = COND string( WHEN sy-subrc = 0 THEN `found` ELSE `not found` ).

" The same search with the ends of the STRING asked for by name. Of the three
" spellings, `\z` is the only one that cannot step over anything.
FIND FIRST OCCURRENCE OF PCRE `\A\d+\z` IN input.
DATA(strict) = COND string( WHEN sy-subrc = 0 THEN `found` ELSE `not found` ).

" And the built-in that asks the whole-value question directly. matches( )
" requires the pattern to account for every character of val, the way Java's
" String.matches and Python's re.fullmatch do, so it needs no anchors at all --
" and it is the form to reach for in new ABAP.
DATA(whole) = COND string( WHEN matches( val = input pcre = `\d+` ) = abap_true
                           THEN `accepted`
                           ELSE `rejected` ).

WRITE / |{ loose } / { strict } / { whole }|.

The two things to carry into ABAP: FIND … PCRE is a search, so ^ and $ in it say nothing about the rest of the value; and matches( val = … pcre = … ) is the whole-value question, the same shape as String.matches and re.fullmatch, and it is the one to reach for.

The anchors, side by side

Nothing in this table is transcribed from a documentation page. Each cell is either an answer one of the programs above printed, or the reading of two of their rows side by side — \A\d+\z against \A\d+\Z against ^\d+$, on a subject built to tell them apart.

Spelling Perl Python re Java Ruby PCRE2 / ABAP Go regexp Rust regex JavaScript
\A start of string start of string start of string start of string start of string start of string start of string not an anchor — the letter A
\z end of string 3.14+ only; error before end of string end of string end of string end of string end of string not an anchor — the letter z
\Z end, or before a final newline end of string (= Perl's \z) end, or before a final terminator end, or before a final newline end, or before a final newline compile error compile error (fancy-regex: Perl's) not an anchor — the letter Z
$ end, or before a final newline end, or before a final newline end, or before a final terminator end of any line end, or before a final newline end of haystack end of haystack end of string
^ start of string start of string start of string start of any line start of string start of haystack start of haystack start of string
with the line flag on $ → end of any line $ → end of any line $ → end of any line already there $ → end of any line $ → end of any line $ → end of any line $ → end of any line

\A is unaffected by the line flag everywhere it exists — that is what makes it the counterpart to ^ rather than a synonym for it.

The measured summary

^\d+$, asked as a search, against the three subjects:

Engine "1" "1\n" "1\nrm -rf /" Safe spelling
Perl 5 match match no \A\d+\z
Python re match match no \A\d+\Z, or re.fullmatch(r"\d+", s)
Java (find()) match match no \A\d+\z, or s.matches("\\d+")
Ruby match match match \A\d+\z
PCRE2 / ABAP PCRE match match no \A\d+\z, or matches( val = … pcre = … )
JavaScript match no no ^\d+$ — until someone adds m
Go regexp match no no \A\d+\z — until someone adds (?m)
Rust regex match no no \A\d+\z — until someone adds (?m)
Rust fancy-regex match no no \A\d+\z

And with the line flag on, every engine in the table accepts "1\nrm -rf /". There is no engine on this page where (?m)^\d+$ is a safe validator.

The rule

For validation, anchor with \A and \z, or use the language's whole-string match. Never with ^…$.

  • Perl, Java, Ruby, PCRE2, Rust, Go: \A…\z.
  • Python: \A…\Z (Perl's \z under Python's name), or re.fullmatch.
  • Java: \A…\z, or String.matches, which anchors for you.
  • ABAP: \A…\z, or matches( val = … pcre = … ).
  • JavaScript: ^…$ is the whole-string form — so the review comment to write is about anyone adding m to a regex that validates.

Two habits make it stick. Strip and reject rather than trust: if a value should not contain a line break, say so explicitly and reject it, instead of hoping an anchor noticed. And reach for the whole-string API before the anchor — re.fullmatch, String.matches, matches( ) — because it cannot be got wrong by someone who has not read this page.

See also

  • Flags, and where you can turn one on — the m/s collision above is one row of a larger table; this is that table, and the inline (?m) form that carries a flag with the pattern instead of beside it.
  • Who supports what — the same engines, probed for eleven other constructs, with yes meaning only that the pattern compiled.
  • Forward references — the other page in this library about one pattern quietly meaning different things in two engines.
  • When there is no lookaround — the Go and Rust habit of doing the rest in the host language, which is also the answer to "should this be a regular expression at all?".
  • Resources — where each engine documents its own anchors: perlre, Python's re, java.util.regex.Pattern's boundary matchers, Ruby's Regexp, pcre2pattern and regexp/syntax. Every one of them states its newline clause, and no two state it in the same words.