_(?!\](?<=\[_\]))
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.