16

I have this file

foo
foo bar
foo bar baz
bar baz
foo baz
baz bar
bar
baz
foo 42
foo bar 42 baz
baz 42

I want to

  1. Select lines which contain foo and do NOT contain bar
  2. Delete lines which contain foo and do NOT contain bar

I read somewhere (can't find the link) that I have to use :exec with | for this.

I tried the following, but it doesn't work

:exec "g/foo" # works
:exec "g/foo" | exec "g/bar" -- first returns lines with foo, then with bar
:exec "g/foo" | :g/bar -- same as above

And ofcourse if I cannot select a line, I cannot execute normal dd on it.

Any ideas?

Edit

Note for the bounty:

I'm looking for a solution that uses proper :g and :v commands, and does not use regex hacks, as the conditions may not be the same (I can have 2 includes, 3 excludes).

Also note that the last 2 examples of things that don't work, they do work for just deleting the lines, but they return incorrect information when I run them without deleting (ie, viewing the selected lines) and they behave as mentioned above.

Rich
  • 5,603
  • 9
  • 39
  • 61
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • I may be misinterpreting you, but don't your points 1 to 5 collapse into simply "delete lines which contain `foo`"? – Zecc Aug 04 '12 at 10:47
  • @Zecc, you're right. I've reduced the points to 2, which I think are necessary.. – Dogbert Aug 04 '12 at 10:50

11 Answers11

10

I'm no vim wizard, but if all you want to do is "Delete lines which contain foo and do NOT contain bar" then this should do (I tried on your example file):

:v /bar/s/.*foo.*//

EDIT: actually this leaves empty lines behind. You probably want to add an optional newline to that second search pattern.

Zecc
  • 4,220
  • 19
  • 17
4

This might still be hackish to you, but you can write some vimscript to make a function and specialized command for this. For example:

command! -nargs=* -range=% G <line1>,<line2>call MultiG(<f-args>)
fun! MultiG(...) range
   let pattern = ""
   let command = ""
   for i in a:000
      if i[0] == "-"
         let pattern .= "\\(.*\\<".strpart(i,1)."\\>\\)\\@!"
      elseif i[0] == "+"
         let pattern .= "\\(.*\\<".strpart(i,1)."\\>\\)\\@="
      else
         let command = i
      endif
   endfor
   exe a:firstline.",".a:lastline."g/".pattern."/".command
endfun

This creates a command that allows you to automate the "regex hack". This way you could do

:G +foo -bar

to get all lines with foo and not bar. If an argument doesn't start with + or - then it is considered the command to add on to the end of the :g command. So you could also do

:G d +foo -bar

to delete the lines, or even

:G norm\ foXp +two\ foos -bar

if you escape your spaces. It also takes a range like :1,3G +etc, and you can use regex in the search terms but you must escape your spaces. Hope this helps.

Conner
  • 30,144
  • 8
  • 52
  • 73
  • This [doesn't seem to be working](http://cl.ly/image/151m2Y3X1c0k/Screen%20Shot%202012-08-14%20at%2010.55.41%20AM.png) for some reason. I ran `:G +end`. – Dogbert Aug 14 '12 at 05:27
  • That's because it's searching for whole words right now. You can remove the `\\<` and `\\>` on the lines with the `pattern .=` to make it search for `end` in `endif`. As is the screenshot you showed doesn't have any matches of " end " (surrounded by spaces). – Conner Aug 14 '12 at 19:51
  • sorry I just had a chance to visit StackOverflow again. Removing those 2 things make this work perfectly. Thanks a lot, bounty coming your way :). Just for reference, I used this: https://gist.github.com/8384390ef05b751c26b8 – Dogbert Oct 19 '12 at 11:39
3

This is where regular expressions get a bit cumbersome. You need to use the zero width match \(search_string\)\@=. If you want to match a list of items in any order, the search_string should start with .* (so the match starts from the start of the line each time). To match a non-occurrence, use \@! instead.

I think these commands should do what you want (for clarity I am using # as the delimiter, rather than the usual /):

  1. Select lines which contain foo and bar:

    :g#\(.*foo\)\@=\(.*bar\)\@=

  2. Select lines which contain foo, bar and baz

    :g#\(.*foo\)\@=\(.*bar\)\@=\(.*baz\)\@=

  3. Select lines which contain foo and do NOT contain bar

    :g#\(.*foo\)\@=\(.*bar\)\@!

  4. Delete lines which contain foo and bar

    :g#\(.*foo\)\@=\(.*bar\)\@=#d

  5. Delete lines which contain foo and do NOT contain bar

    :g#\(.*foo\)\@=\(.*bar\)\@!#d

Prince Goulash
  • 15,295
  • 2
  • 46
  • 47
3

You won't achieve your requirements unless you're willing to use some regular expressions since the expressions are what drives :global and it's opposite :vglobal.

This is no hacking around but how the commands are supposed to work: they need an expression to work with. If you're not willing to use regular expressions, I'm afraid you won't be able to achieve it.

Answer terminates here if you're not willing to use any regular expressions.


Assuming that we are nice guys with an open mind, we need a regular expression that is true when a line contains foo and not bar.

Suggestion number 5 of Prince Goulash is quite there but doesn't work if foo occurs after bar.

This expression does the job (i.e. print all the lines):

:g/^\(.*\<bar\>\)\@!\(.*\<foo\>\)\@=/

If you want to delete them, add the delete command:

:g/^\(.*\<bar\>\)\@!\(.*\<foo\>\)\@=/d

Description:

  • ^ starting from the beginning of the line
  • \(.*\<bar\>\) the word bar
  • \@! must never appear
  • \(.*\<foo\>\)\@= but the word foo has to appear anywhere on the line

The two patterns could also be swapped:

:g/^\(.*\<foo\>\)\@=\(.*\<bar\>\)\@!/

yields the same results.

Tested with the following input:

01      foo
02      foo bar
03      foo bar baz
04      bar baz
05      foo baz
06      baz bar
07      bar
08      baz
09      foo 42
10      foo bar 42 baz
11      42 foo baz
12      42 foo bar
13      42 bar foo
14      baz 42
15      baz foo
16      bar foo

Regarding multiple includes/excludes:

Each exclude is made of the pattern

\(.*\<what_to_exclude\>\)\@!

Each include is made of the pattern

\(.*\<what_to_include\>\)\@=

To print all the lines that contain foo but not bar nor baz:

g/^\(.*\<bar\>\)\@!\(.*\<baz\>\)\@!\(.*\<foo\>\)\@=/

Print all lines that contain foo and 42 but neither bar nor baz:

g/^\(.*\<bar\>\)\@!\(.*\<baz\>\)\@!\(.*\<foo\>\)\@=\(.*\<42\>\)\@=/

The sequence of the includes and excludes is not important, you could even mix them:

g/^\(.*\<bar\>\)\@!\(.*\<42\>\)\@=\(.*\<baz\>\)\@!\(.*\<foo\>\)\@=/
Community
  • 1
  • 1
eckes
  • 64,417
  • 29
  • 168
  • 201
2

One might think a combination like :g/foo/v/bar/d would work, but unfortunately this isn't possible, and you will have to recur to one of the proposed work-arounds.

As described in the help, behind the scenes the :global command works in two stages,

  • first marking the lines on which to operate,
  • then performing the operation on them.

Out of interest, I had a look at the relevant parts in the Vim source: In ex_cmds.c, ex_global(), you will find that the global flag global_busy prevents repeated execution of the command while it is busy.

glts
  • 21,808
  • 12
  • 73
  • 94
  • 1
    +1 for referencing the Vim source. It's a great shame the global commands cannot be used recursively, as the syntax would be very intuitive. – Prince Goulash Aug 08 '12 at 09:26
0

You want to employ a negative look ahead. This article gives more or less the specific example you are trying to achieve. http://www.littletechtips.com/2009/09/vim-negative-match-using-negative-look.html

I changed it to :g/foo(.*bar)\@!/d

Please let us know if you consider this a regex hack.

Pat O
  • 1,344
  • 3
  • 12
  • 27
0

I will throw my hat in the ring. As vim's documentation explicitly states recursive global commands are invalid and the regex solution will get pretty hairy quickly, I think this is job for a custom function and command. I have created the :G command.

The usage is as :G followed by patterns surrounded by /. Any pattern that should not match is prefixed with a !.

:G /foo/ !/bar/ d

This will delete all lines that match /foo/ and does not match /bar/

:G /42 baz/ !/bar/ norm A$

This will append a $ to all lines matching /42 baz/ and that don't match /bar/

:G /foo/ !/bar/ !/baz/ d

This will delete all lines that match /foo/ and does not match /bar/ and does not match /baz/

The script for the :G command is below:

function! s:ManyGlobal(args) range
  let lnums = {}
  let patterns = []
  let cmd = ''
  let threshold = 0
  let regex = '\m^\s*\(!\|v\)\=/.\{-}\%(\\\)\@<!/\s\+'

  let args = a:args
  while args =~ regex
    let pat = matchstr(args, regex)
    let pat = substitute(pat, '\m^\s*\ze/', '', '')
    call add(patterns, pat)
    let args = substitute(args, regex, '', '')
  endwhile

  if args =~ '\s*'
    let cmd = 'nu'
  else
    let cmd = args
  endif

  for p in patterns
    if p =~ '^(!\|v)'
      let op = '-'
    else
      let op = '+'
      let threshold += 1
    endif
    let marker = "let l:lnums[line('.')] = get(l:lnums, line('.'), 0)" . op . "1"
    exe a:firstline . "," . a:lastline . "g" . substitute(p, '^(!\|v)', '', '') . marker
  endfor

  let patterns = []
  for k in keys(lnums)
    if threshold == lnums[k]
      call add(patterns, '\%' . k . 'l')
    endif
  endfor

  exe a:firstline . "," . a:lastline . "g/\m" . join(patterns, '\|') . "/ " . cmd
endfunction

command! -nargs=+ -range=% G <line1>,<line2>call <SID>ManyGlobal(<q-args>)

The function basically parses out the arguments then goes and marks all matching lines with each given pattern separately. Then executes the given command on each line that is marked the proper amount of times.

Peter Rincker
  • 43,539
  • 9
  • 74
  • 101
  • Strange. Doesn't work for me. I ran it on the code you have above, and ran the following command: `G /end/ !/if/`, and it returns all the lines containing `end`. Am I doing something wrong? – Dogbert Aug 14 '12 at 05:31
  • Make sure you supply a command so: `:G /end/ !/if/ nu` for example – Peter Rincker Aug 14 '12 at 14:42
0

All right, here's one which actually simulates recursive use of global commands. It allows you to combine any number of :g commands, at least theoretically. But I warn you, it isn't pretty!

Solution to the original problem

I use the Unix program nl (bear with me!) to insert line numbers, but you can also use pure Vim for this.

:%!nl -b a
:exec 'norm! qaq'|exec '.,$g/foo/d A'|exec 'norm! G"apddqaq'|exec '.,$v/bar/d'|%sort|%s/\v^\s*\d+\s*

Done! Let's see the explanation and general solution.

General solution

This is the approach I have chosen:

  • Introduce explicit line numbering
  • Use the end of the file as a scratch space and operate on it repeatedly
  • Sort the file, remove the line numbering

Using the end of the file as a scratch space (:g/foo/m$ and similar) is a pretty well-known trick (you can find it mentioned in the famous answer number one). Also note that :g preserves relative ordering of the lines – this is crucial. Here we go:

Preparation: Number lines, clear "accumulator" register a.

:%!nl
qaq

The iterative bit:

  1. :execute global command, collect matching lines by appending them into the accumulator register with :d A.
  2. paste the collected lines at the end of the file
  3. repeat for range .,$ (the scratch space, or in our case, the "match" space)

Here's an extended example: delete lines which do contain 'foo', do not contain 'bar', do contain '42' (just for the demonstration).

:exec '.,$g/foo/d A' | exec 'norm! G"apddqaq' | exec '.,$v/bar/d A' | exec 'norm! G"apddqaq' | exec '.,$g/42/d A' | exec 'norm! G"apddqaq'
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 (this is the repeating bit)

When the iterative bit ends, the lines .,$ contain the matches for your convenience. You can delete them (dVG) or whatever.

Cleanup: Sort, remove line numbers.

:%sort
:%s/\v^\s*\d+\s*



I'm sure other people can improve on the details of the solution, but if you absolutely need to combine multiple :gs and :vs into one, this seems to be the most promising solution.

Community
  • 1
  • 1
glts
  • 21,808
  • 12
  • 73
  • 94
0

The in-built solutions looks very complex. One easy way would be to use LogiPat plugin:

Doc: http://www.drchip.org/astronaut/vim/doc/LogiPat.txt.html

Plugin: http://www.drchip.org/astronaut/vim/index.html#LOGIPAT

With this, you can easily search for patterns. For e.g, to search for lines containing foo, and not bar, use:

:LogiPat "foo"&!("bar")

This would highlight all the lines matching the logical pattern (if you have set hls). That way you can cross-check whether you got the correct lines, and then traverse with 'n', and delete with 'dd', if you wish.

m.divya.mohan
  • 2,261
  • 4
  • 24
  • 34
0

I realize you explicitly stated that you want solutions using :g and :v, but I firmly believe this is a perfect example of a case where you really should use an external tool.

:%!awk '\!/foo/ || /bar/'

There's no need to re-invent the wheel.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
0

Select lines which contain foo and do NOT contain bar

Delete lines which contain foo and do NOT contain bar

This can be done by combining global and substitute commands:

:v/bar/s/.*foo.*//g
Community
  • 1
  • 1
builder-7000
  • 7,131
  • 3
  • 19
  • 43