Anchoring to a line without turning on the m flag
Lookarounds let you assert a line boundary at one specific position: (?<=\n|^) for the start of a line and (?=\n|$) for the end, without the m flag rewriting every anchor in the pattern.
At a glance
- Start of line assertion
- (?<=\n|^)
- End of line assertion
- (?=\n|$)
- Windows text
- (?=\r?\n|$), so the carriage return stays outside the match
- Width
- zero, none of these consume a character
- Why not the m flag
- m rewrites every ^ and $ in the pattern, including ones inside other assertions
- Lookbehind support
- ES2018, and absent from RE2 based engines such as Go's regexp package
In detail
The situation this solves is narrow and common: a pattern already uses ^ and $ to mean the two ends of the whole input, and one part of it also needs to know where a line starts or stops. The m flag is an all or nothing switch, so using it forces you to rewrite the anchors you were happy with.
A lookbehind for a newline or the start of input does the start of line job, and a lookahead for a newline or the end of input does the other end. Both are zero width, so the match itself contains only the line content. Against "alpha\nbeta\ngamma" the pattern /(?<=\n|^)\w+(?=\n|$)/g returns the three words with no line terminators attached.
On Windows text the naive lookahead leaves the carriage return inside the match, because the assertion fires at the \n and the \r is already behind the cursor. The captured line then ends in an invisible character that survives comparison, gets written back to a file, and shows up much later as a value that looks equal to another one but is not. Writing (?=\r?\n|$) keeps it out.
The cost is portability. Lookbehind arrived in ES2018 and is missing from engines built on RE2, so a pattern that also has to compile in Go needs a different structure entirely: split the text on line terminators first and run a simpler pattern per line.
Examples
const text = "alpha\nbeta\ngamma";
text.match(/(?<=\n|^)\w+(?=\n|$)/g); // ["alpha", "beta", "gamma"]
// Just the last line, still no flags.
text.match(/(?<=\n)[^\n]*$/)[0]; // "gamma""a\r\nb".match(/\w+(?=\r?\n|$)/g); // ["a", "b"]Without the \r? the first match would end in an invisible carriage return.
Things that catch people out
- (?=\n|$) on Windows text leaves the \r inside the match, so the captured line ends with an invisible character that breaks a later equality check.
- A ^ written inside a lookbehind still obeys the m flag, so turning m on for some other part of the pattern changes what the assertion means.
- These assertions are zero width. Matching against them alone with the g flag produces an empty match at every line boundary rather than returning the lines.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →The m flag: multiline anchors
What multiline mode changes about ^ and $, which line terminators JavaScript counts, and why the m flag never makes the dot match a newline.
(?<=...): positive lookbehind and where it runs
JavaScript allows variable length lookbehind, which Java does not, and matches its contents from right to left. RE2 based engines have none at all.
\A and \z in JavaScript: what to use instead
JavaScript has no \A, \z or \Z. Here is what those anchors silently turn into, and the absolute start and end assertions that survive the m flag.
(...) capturing groups: numbering and what comes back
How capture groups are numbered, what a group that did not participate returns, and why quantifying a group throws away everything but the last pass.
(?:...): grouping without creating a capture
A non capturing group applies quantifiers and alternation without taking a number, which is what keeps $1 pointing at the same thing after an edit.
(?<name>...): named capture groups
Named groups survive being reordered, read as documentation and give you $<name> in replacements. They are still numbered underneath.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links