Vim Tips Wiki
Register
Advertisement
Tip 478 Printable Monobook Previous Next

created 2003 · complexity basic · version 7.0


Several methods allow you to copy all search matches (search hits). Usually, you want to copy each line which contains text matching your search pattern, but you can also copy just the matching text. The tip starts with methods to easily list search hits, without copying them, then shows how to copy matches, or matching lines. Similar techniques, also shown below, can be used to delete all matching lines.

Copying or deleting search matches is easy when simple search patterns are used. The techniques shown here also work with multiline matches.

Search within a file

To search, press / then type what you want to find, or press * to search for the current word.

To list all lines matching your search pattern, use the following. The first command lists lines matching your last search. The second lists lines matching pattern.

:g/
:g/pattern

On some systems, at the "Press ENTER or type command to continue" prompt, you can select displayed text with the mouse then press Ctrl-Y to copy the selection to the clipboard. :help press-enter

To view a window of search results, see Find in files within Vim. You can use % for the file path to search only the current file, for example:

" Save file, search it for 'pattern', and open a clickable list.
:w
:vimgrep /pattern/ %
:copen

To view only the parts of a file that match your pattern, see Folding with Regular Expression. You can fold away non-matching lines, and use zr to reveal more context around the search hits.

Another approach is to put the cursor on a word of interest, then type [I to list occurrences.

Copy or delete matching lines

Matching lines can be copied to the clipboard, so they can be pasted into another application.

To copy all lines containing a search pattern, use the following (see here). The first command clears register a (:help q). The second appends all matching lines to that register (:help quotea). The third copies register a to the clipboard (register +) for easy pasting into another application. Replace pattern with what you want to search for, or omit it (:g//y A) to use the last search.

qaq
:g/pattern/y A
:let @+ = @a

The above procedure captures only the first line of each match. See below for copying multiline matches.

It is also easy to delete lines containing single-line matches. The first of the following commands deletes all lines containing pattern, while the second deletes all lines matching the last search:

:g/pattern/d
:g//d

Use :v//d to delete all lines that do not match the last search pattern, or :v/pattern/d to delete all lines that do not match the given pattern.

If a search pattern may find multiline matches, the above procedures to delete matching lines fail. In that case, use the following which works with any kind of match.

" Delete all lines in given range that contain a match, or part of a match.
" :DeleteLines      delete all lines matching last search
" :DeleteLines pat  delete all lines matching the given pattern 'pat'
" The deleted lines are NOT saved anywhere. Works with multiline matches.
function! DeleteLines(pattern) range
  let delid = '<!DELETE!LINE!ID!>'  " an id that does not occur in buffer
  if search(delid, 'cnw') > 0
    redraw  " so message is seen
    echo 'Error: buffer contains pattern used to delete lines'
    return
  endif
  let pattern = empty(a:pattern) ? @/ : a:pattern
  let sub = a:firstline . ',' . a:lastline . 's/' . escape(pattern, '/')
  " Retain newline if it is last character so do not delete following line.
  let rep = '/\=delid . (submatch(0) =~ "\n$" ? "\r" : "")/e'
  execute sub . rep . (&gdefault ? '' : 'g')
  execute 'g/\C' . delid . '/' . 'd'
endfunction
command! -nargs=? -range=% DeleteLines k'|<line1>,<line2>call DeleteLines(<q-args>)

Copy matches

The simple script shown here copies only the text that matches search hits. It works with multiline matches. First save the buffer because the script changes it (the changes should result in the same text, but it is safer to save first). After sourcing the following, search for a pattern, then enter :CopyMatches to copy all matches to the clipboard, or :CopyMatches x where x is any register to hold the result.

function! CopyMatches(reg)
  let hits = []
  %s//\=len(add(hits, submatch(0))) ? submatch(0) : ''/ge
  let reg = empty(a:reg) ? '+' : a:reg
  execute 'let @'.reg.' = join(hits, "\n") . "\n"'
endfunction
command! -register CopyMatches call CopyMatches(<q-reg>)

The above code works after any search, including searches which match text in more than one line. The expression used in the substitute replacement uses len() as a trick to call add() (which returns the list after appending a match to it).

Use the following to copy matches from all buffers to register a:

:let @a = ''
:bufdo CopyMatches A

In the following text, use search pattern \<A\_.\{-}Z\> (any word beginning with A, followed by any characters including newline, as few as possible, to a word ending with Z) to test :CopyMatches (it copies four hits: A1...1Z and A2...2Z and A3...3Z and A4...4Z):

Test text bbb ccc A1 ddd eee
Afake fff1Z A2 ggg2Z hhh A3 iii
jjj3Z
Nothing here.
More A4 kkk lll4Z

Copy matches or lines

The script in this section provides more features. It works with multiline matches, and provides a CopyMatches command to copy only matching text, as well as a CopyLines command to copy all lines that contain a match.

Each command accepts a range of lines, and can copy into any register (by default, the clipboard). The first of the following commands copies all text matching the last search into the clipboard, while the second copies all matches in lines 1 to 10 inclusive to register a, and the third copies matches from the current line (.) to the default register ("):

:CopyMatches
:1,10CopyMatches a
:.CopyMatches "

Matches can be copied into a scratch buffer instead of a register. To do that, use - as the register name. If a bang (exclamation mark) is used, the line number of each match is included. Also, a search pattern can be entered on the command line. For example, the following commands display the results for two searches in the same scratch buffer. Search matches, with line numbers, are displayed for all words beginning with 'a', and for all strings of digits. Enter the first command, then switch back to the original buffer while keeping the scratch window open, then enter the second command:

:CopyMatches! - \<a\w*
:CopyMatches! - \d\+

Create file ~/.vim/plugin/copymatches.vim (Unix) or $HOME/vimfiles/plugin/copymatches.vim (Windows) containing the script below, then restart Vim.

" Plugin to copy matches (search hits which may be multiline).
" Version 2012-05-03 from http://vim.wikia.com/wiki/VimTip478
"
" :CopyMatches      copy matches to clipboard (each match has newline added)
" :CopyMatches x    copy matches to register x
" :CopyMatches X    append matches to register x
" :CopyMatches -    display matches in a scratch buffer (does not copy)
" :CopyMatches pat  (after any of above options) use 'pat' as search pattern
" :CopyMatches!     (with any of above options) insert line numbers
" Same options work with the :CopyLines command (which copies whole lines).

" Jump to first scratch window visible in current tab, or create it.
" This is useful to accumulate results from successive operations.
" Global function that can be called from other scripts.
function! GoScratch()
  let done = 0
  for i in range(1, winnr('$'))
    execute i . 'wincmd w'
    if &buftype == 'nofile'
      let done = 1
      break
    endif
  endfor
  if !done
    new
    setlocal buftype=nofile bufhidden=hide noswapfile
  endif
endfunction

" Append match, with line number as prefix if wanted.
function! s:Matcher(hits, match, linenums, subline)
  if !empty(a:match)
    let prefix = a:linenums ? printf('%3d  ', a:subline) : ''
    call add(a:hits, prefix . a:match)
  endif
  return a:match
endfunction

" Append line numbers for lines in match to given list.
function! s:MatchLineNums(numlist, match)
  let newlinecount = len(substitute(a:match, '\n\@!.', '', 'g'))
  if a:match =~ "\n$"
    let newlinecount -= 1  " do not copy next line after newline
  endif
  call extend(a:numlist, range(line('.'), line('.') + newlinecount))
  return a:match
endfunction

" Return list of matches for given pattern in given range.
" If 'wholelines' is 1, whole lines containing a match are returned.
" This works with multiline matches.
" Work on a copy of buffer so unforeseen problems don't change it.
" Global function that can be called from other scripts.
function! GetMatches(line1, line2, pattern, wholelines, linenums)
  let savelz = &lazyredraw
  set lazyredraw
  let lines = getline(1, line('$'))
  new
  setlocal buftype=nofile bufhidden=delete noswapfile
  silent put =lines
  1d
  let hits = []
  let sub = a:line1 . ',' . a:line2 . 's/' . escape(a:pattern, '/')
  if a:wholelines
    let numlist = []  " numbers of lines containing a match
    let rep = '/\=s:MatchLineNums(numlist, submatch(0))/e'
  else
    let rep = '/\=s:Matcher(hits, submatch(0), a:linenums, line("."))/e'
  endif
  silent execute sub . rep . (&gdefault ? '' : 'g')
  close
  if a:wholelines
    let last = 0  " number of last copied line, to skip duplicates
    for lnum in numlist
      if lnum > last
        let last = lnum
        let prefix = a:linenums ? printf('%3d  ', lnum) : ''
        call add(hits, prefix . getline(lnum))
      endif
    endfor
  endif
  let &lazyredraw = savelz
  return hits
endfunction

" Copy search matches to a register or a scratch buffer.
" If 'wholelines' is 1, whole lines containing a match are returned.
" Works with multiline matches. Works with a range (default is whole file).
" Search pattern is given in argument, or is the last-used search pattern.
function! s:CopyMatches(bang, line1, line2, args, wholelines)
  let l = matchlist(a:args, '^\%(\([a-zA-Z"*+-]\)\%($\|\s\+\)\)\?\(.*\)')
  let reg = empty(l[1]) ? '+' : l[1]
  let pattern = empty(l[2]) ? @/ : l[2]
  let hits = GetMatches(a:line1, a:line2, pattern, a:wholelines, a:bang)
  let msg = 'No non-empty matches'
  if !empty(hits)
    if reg == '-'
      call GoScratch()
      normal! G0m'
      silent put =hits
      " Jump to first line of hits and scroll to middle.
      ''+1normal! zz
    else
      execute 'let @' . reg . ' = join(hits, "\n") . "\n"'
    endif
    let msg = 'Number of matches: ' . len(hits)
  endif
  redraw  " so message is seen
  echo msg
endfunction
command! -bang -nargs=? -range=% CopyMatches call s:CopyMatches(<bang>0, <line1>, <line2>, <q-args>, 0)
command! -bang -nargs=? -range=% CopyLines call s:CopyMatches(<bang>0, <line1>, <line2>, <q-args>, 1)

GetMatches() alternative

The following shows an alternative method to copy matches. This script is less useful than others in this tip as it does not handle multiline matches. However, the technique may be of interest as it shows how to use the match() function. In addition, it does not involve changes to the current buffer.

" Return list of matches for given pattern in given range.
" This only works for matches within a single line.
" Empty hits are skipped so search for '\d*\ze,' is not stuck in '123,456'.
" If omit match() 'count' argument, pattern '^.' matches every character.
" Using count=1 causes text before the 'start' argument to be considered.
function! GetMatches(line1, line2, pattern)
  let hits = []
  for line in range(a:line1, a:line2)
    let text = getline(line)
    let from = 0
    while 1
      let next = match(text, a:pattern, from, 1)
      if next < 0
        break
      endif
      let from = matchend(text, a:pattern, from, 1)
      if from > next
        call add(hits, strpart(text, next, from - next))
      else
        let char = matchstr(text, '.', next)
        if empty(char)
          break
        endif
        let from = next + strlen(char)
      endif
    endwhile
  endfor
  return hits
endfunction

See also

Comments

Just after my last major update to this tip (a week ago), I found a note pointing to a vim_use thread where ZyX showed a simple yet brilliant technique for handling multiline matches. Testing shows that the posted code is not entirely correct as it misses multiline matches that occur in the same line after a short match. However, I have done extensive modifications and the resulting script (now in this tip) is sensational. JohnBeckett 03:31, May 3, 2012 (UTC)

Advertisement