_(?!\](?<=\[_\]))
If the underscore isn't followed by a closing bracket, the negative lookahead succeeds immediately. Otherwise, it does a lookbehind to find out if the underscore is also preceded by an opening bracket. You can replace the "_]" with dots to make it clear that you're only interested in the opening bracket this time:
_(?!\](?<=\[..))
You can do the lookbehind first if you want:
_(?<!\[_(?=\]))
The important thing is that the second lookaround has to be nested within the first one in order to achieve the "NOT (x AND y)
" semantics.
Testing it in EditPad Pro, it matches the underscore in all but the last of these strings:
test_test
test[_test
test_]
_]Test
Test[_
test[_]test
EDIT: here's an easier-to-read version:
(?<!\[)_|_(?!\])
What I like about the nested-lookaround version is that it doesn't do anything until it actually finds an underscore. Unless the regex engine is smart enough optimize it away, this "(NOT x) OR (NOT y)
" version will do a negative lookbehind at every single position.