Skip to content

$ — the end, plus one newline you did not ask for

One line: $ matches at the end of the subject — and also just before a final newline in Python, Perl, Java and Ruby, and at the end of every line in Ruby, and only at the true end in JavaScript, Go and Rust; three definitions of "end" behind one character, none of them an error.

Level: reference · one construct, eight engines

What it means

Where $ succeeds Python, Perl, Java Ruby JavaScript, Go, Rust regex, fancy-regex
End of the subject yes yes yes
Before a final \n yes yes no
Before any \n only under (?m) / re.M / MULTILINE yes, always only under (?m) or the m flag

The middle column is the Perl heritage: $ was designed for lines read with their newline still attached, so /foo$/ would match a line that said foo. Every engine that copied Perl's syntax had to decide whether to copy that, and they split three ways.

Measured

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

construct                python  perl  node  ruby  java  go  rust  fancy
plain end                a       a     a     a     a     a   a     a
before a final newline   a       a     no    a     a     no  no    no
before a middle newline  no      no    no    a     no    no  no    no
(?m) line end            a       a     -     a     a     a   a     a
node m flag              no      no    a     a     no    no  no    no
\z instead               -       no    no    no    no    no  no    no
\z in JavaScript         -       no    az    no    no    no  no    no
escaped dollar           $5      $5    $5    $5    $5    $5  $5    $5
mid-pattern $            no      no    no    no    no    no  no    no

Reading the table

  • Row 2, a$ against a\n. Python, Perl, Ruby and Java say a: the newline is stepped over. JavaScript, Go and both Rust crates say no. Same pattern, same subject, and a form field that ends in a newline is accepted by one group and rejected by the other.
  • Row 3, a$ against a\nb. Only Ruby matches — its $ is a line anchor. The other four "newline-tolerant" engines tolerate exactly one newline, and only at the very end. That distinction is why "1\nrm -rf /" does not get through ^\d+$ in Python, and does in Ruby; the lesson page runs the payloads.
  • Rows 4 and 5. (?m) — or the m flag in JavaScript, which cannot spell an inline flag — turns $ into a line anchor everywhere. After that, all eight agree.
  • Rows 6 and 7, \z. The anchor that means the true end answers no for a\n in every engine that has it. Python's column is -: Python 3.12 has no \z (3.14 added it — the probe runs 3.12 because CI does). JavaScript's no in row 6 is not agreement, and row 7 is the tell: a\z matches the text az, because \z is the letter z there.
  • Row 8. \$ is a literal dollar everywhere. Row 9: an unescaped $ in the middle of a pattern is an anchor in the middle of a pattern, which can never succeed — a$b matches nothing in all eight.

The trap

The subject with a trailing newline. Files end in one, input() in some languages strips it and in others does not, and HTTP form fields can carry one. ^\d+$ was written to accept a number; in four of these eight engines it also accepts a number followed by a newline, and code downstream that does int(value) succeeds while code that does value == "42" fails. Nothing in the pattern says which you get.

The rule that survives every engine here: for validation, anchor with \A and \z where they exist, or use the language's whole-string call — re.fullmatch, String.matches, \A…\z in Ruby — and never ^…$.

Where it is measured in depth

See also