Regex Negative Lookahead Is Being Ignored

Viewed 137

I have the following sample text

[Item 1](./path/notes.md)
[Item 2](./path)
[Item 3](./path/notes.md)
[Item 4](./path)

When I applied the following regex \[(.*)\].*(?!notes\.md).*\), I expected the following output when I printed the first capture group

Item 2
Item 4

but what I ended up getting was

Item 1
Item 2
Item 3
Item 4

which, to me, seems that the negative lookahead portion, (?!notes\.md), was being ignored for some reason, so the regex was matching the whole string.

What is really confusing me, is that a positive lookahead works as I would expect. For example using \[(.*)\].*(?=notes\.md).*\) returns the following when printing the first capture group

Item 1
Item 3

which makes sense, so I am really confused as to why the negative lookahead is not functioning properly.

4 Answers

In short, you have way too many .* (can lead to Catastrophic backtracking, look it up!). Remember that they match any character zero or more times. That means they will keep trying to match until they get success. And that is not necessarily the number of characters, you want.

A way to solve your problem is to move the negative look ahead to the front, like this:

(?!.*notes\.md)\[([^\]]+)\].*

Explanation:

(?!.*notes\.md) negative look ahead for any number of any char followed by 'notes.md'

\[ a [ character

([^\]]+) group 1, any character not being ], one or more times

\] a ] character

.* the rest of the line

Use the 'multiline' flag to get each line.

Let's go through what happens when matching your pattern on item 1:

  • \[(.*)\] matches [Item 1]
  • .* matches (./path/notes.md
  • The remaining string is now )
  • (?!notes\.md) checks whether the remaining string matches the pattern notes\.md. It does not, so the match continues.
  • \) matches the ) and the match succeeds.

If you change it so that the .* before the lookahead is inside the lookahead instead (\[(.*)\](?!.*notes\.md).*\)), it will now work as follows:

  • \[(.*)\] matches [Item 1]
  • The remaining string is now (./path/notes.md)
  • (?!.*notes\.md) checks whether the remaining string matches the pattern .*notes\.md, which it does, so the match fails (well more precisely, the regex engine will try to backtrack before it gives up on the match, but there's no alternative way to match \[(.*)\]', so the match still fails).

So with that change, it will reject all strings where notes.md appears anywhere before the ). If you want it to only reject instances where notes.md appears directly before the ), you can use a loookbehind (without .*) instead or add the \) to the lookahead.

The problem here is that .* before the negative lookahead is greedy and will continue finding anything then stops.

A way to manage this is to include this greedy behaviour inside of the negative lookahead like hère

https://regex101.com/r/yzUQoP/1

/\[(.*)\](?!.*notes\.md)/gm

The pattern \[(.*)\].*(?!notes\.md).*\) that you tried matches from the first [ until the last occurrence of ]

Then what happens is that .* will match the rest of the line, so the following assertion (?!notes\.md) will always be true as the rest of the line is already matched.

Then the engine can backtrack to match the last )


If you don't want to cross [] and () while matching:

\[([^][]+)]\((?![^()]*\bnotes\.md\b)[^()]*\)
  • \[ Match [
  • ([^][]+) Capture group 1, match 0+ times any char other than [ and ]
  • ]\( Match ](
  • (?! Negative lookahead
    • [^()]*\bnotes\.md\b Match 0+ times any char other then ( and ) and then match notes.md between word boundaries to prevent partial matches
  • ) Close lookahead
  • [^()]* Match 0+ times any char except ( and )
  • \) Match )

Regex demo

Related