T.J. Crowder
T.J. Crowder

Reputation: 1074295

How do you match a pattern skipping exceptions?

In vim, I'd like to match a regular expression in a search and replace operation, but with exceptions — a list of matches that I want to skip.

For example, suppose I have the text:

-one- lorem ipsum -two- blah blah -three- now is the time -four- the quick brown -five- etc. etc.

(but with lots of other possibilities) and I want to match -\(\w\+\)- and replace it with *\1* but skipping over (not matching) -two- and -four-, so the result would be:

*one* lorem ipsum -two- blah blah *three* now is the time -four- the quick brown *five* etc. etc.

It seems like I should be able to use some kind of assertion (lookbehind, lookahead, something) for this, but I'm coming up blank.

Upvotes: 3

Views: 81

Answers (2)

Ingo Karkat
Ingo Karkat

Reputation: 172570

The negative lookahead in my other answer is the direct solution, but its syntax is a bit complex (and there can be patterns where the rules for the delimiter are not so simple, and the result then is much less readable).

As you're using substitution, an alternative is to put the selection logic into the replacement part of :substitute. Vim allows a Vimscript expression in there via :help sub-replace-expression.

We have the captured word in submatch(1) (equivalent to \1 in a normal replacement), and now just need to check for the two excluded words; if it's one of those, do a no-op substitution by returning the original full match (submatch(0)), else just return the captured group.

:substitute/-\(\w\+\)-/\=submatch(index(['two', 'four'], submatch(1)) == -1 ? 1 : 0)/g

It's not shorter than the lookahead pattern (well, we could golf the pattern and drop the ternary operator, as a boolean is represented by 0/1, anyway), so here I would still use the pattern. But in general, it's good to know that there's more than one way to do it :-)

Upvotes: 2

Ingo Karkat
Ingo Karkat

Reputation: 172570

You're looking for a negative lookahead assertion. In Vim, that's done via :help /\@!, like (?!pattern) in Perl.

Basically, you say don't match FOO here, and in general match word characters:

/-\(\%(FOO\)\@!\w\+\)-/

Note how I'm using non-capturing groups (:help /\%(). What's still missing is an assertion on the end, so the above would also exclude -FOOBAR-. As we have a unique end delimiter here, it's easiest to append that:

/-\(\%(FOO-\)\@!\w\+\)-/

Applied to your example, you just need to introduce two branches (for the two exclusions) in place of FOO, and you're done:

/-\(\%(two-\|four-\)\@!\w\+\)-/

Or, by factoring out the duplicated end delimiter:

/-\(\%(\%(two\|four\)-\)\@!\w\+\)-/

This matches any word characters in between dashes, except if those words form either two or four.

Upvotes: 3

Related Questions