65

In Vim, is there a way to search for lines that match say abc but do not also contain xyz later on the line? So the following lines would match:

The abc is the best
The first three letters are abc

and the following would not match:

The abc is the best but xyz is cheaper
The first three letters are abc and the last are xyz

I know about syntax like the following:

/abc\(xyz\)\@!

but that only avoids matching abcxyz and not if there is anything in between, such as abc-xyz. Using

/abc.*\(xyz\)\@!

also does not work because there are many positions later in the line where xyz is not matched.

(I should note that on the command line I would do something like grep abc <infile | grep -v xyz but I would like to do the above interactively in Vim.)

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285

6 Answers6

68

The .* allows an arbitrary distance between the match and the non-match, which is asserted later.
You need to pull the .* into the negative look-ahead:

/abc\(.*xyz\)\@!

Possible Reason
The non-match is attempted for all possible matches of .*, and the \@! is declared as fulfilled only when all branches have been tried.

Good Pen
  • 625
  • 6
  • 10
Ingo Karkat
  • 167,457
  • 16
  • 250
  • 324
  • 4
    This is equivalent to `abc(?!.*xyz)` in Perl-like engines. – nhahtdh Jan 15 '14 at 21:37
  • And this works, because look-ahead simple launches another full-blown search from the current position for the pattern, and use the result to decide whether to continue or not (depending whether the look-ahead is positive or negative). – nhahtdh Jan 15 '14 at 21:47
  • 1
    also see `:help perl-patterns`. For `pattern2` not preceded by `pattern1`, that is negative look-behind, search for `\(pattern1\)\@<!pattern2`. – hochl Feb 22 '17 at 12:10
  • This might be outdated - but it seems to me that the above solution will not work for a string like `xyzabc`. The OP indicated that a line should not have `xyz` *anywhere* in the line. What worked for me is mandated capturing the whole line: `^(?!.*xyz).*abc(?!.*xyz)` (not `vim` format). See [RegExr](https://regexr.com/3if0r) – Guy Dec 25 '17 at 12:08
  • This helped me search for change lines in a Terraform plan `/\("[^"]*"\) => \(\1\)\@!` – Bruno Bronosky Jul 30 '19 at 11:17
24

I always find it weird, that you need to escape brackets in vim, so I try to use the "very magic" mode most of the time, activated with \v:

/\vabc(.*xyz)@!
hansaplast
  • 11,007
  • 2
  • 61
  • 75
9

Although your question is about lookahead, I found it while searching for lookbehind. Thus, I post the solution to lookbehind so that others may find it.

If you search for pattern2 not preceded by pattern1, that is negative lookbehind, search for:

\(pattern1\)\@<!pattern2
hochl
  • 12,524
  • 10
  • 53
  • 87
2

This works for me /abc\(.*xyz\)\@!

priomsrb
  • 2,602
  • 3
  • 26
  • 34
1

If you aren't strict about only using vim regex search, there is another option. You can use global command as following

:g/abc/v/xyz/p

Explanation

  • "g/abc" : scan all lines and mark all matching the pattern
  • v/xyz/ : from above results mark all lines not including "xyz" pattern
  • p : print the lines found. You can replace it with another command
caltuntas
  • 10,747
  • 7
  • 34
  • 39
0

I think I'd write this as /abc\(\(xyz\)\@!.\)*$

I'm not sure whether this is faster than other suggestions (I think it might be since it tests each position only once) but it makes conceptual sense as:

"abc followed by anything which is not xyz, any number of times until end of line"

I like that better than "abc followed by anything not followed by xyz" as in the other patterns mentioned.

Ben
  • 8,725
  • 1
  • 30
  • 48