콘텐츠로 건너뛰기
Home » vim에서 여러 파일에 찾기/바꾸기

vim에서 여러 파일에 찾기/바꾸기

리눅스 쉘에서, 여러 텍스트 파일의 내용들 중 특정한 단어나 패턴을 찾는데 사용되는 유틸리티인 grep 이라는 프로그램이 있습니다. vim에서도 grep과 같이 여러 파일에서 단어나 패턴을 한꺼번에 찾을 수 있는 기능을 제공합니다. vim의 이러한 기능은 다시 두 가지로 나뉩니다. 첫째로 vim은 그 자체로 grep과 비슷한 기능을 제공하도록 구현되어 있어서 내장된 :vimgrep 이라는 명령을 사용합니다. 이 방식은 vim만 있으면 사용할 수 있고, 플랫폼마다 개행문자가 다르게 쓰이는 부분이나 파일 인코딩을 자동으로 처리해주는 장점이 있습니다만, 속도가 느리다는 단점이 있습니다. 두번째 방법으로는 아예 grep을 사용하여 텍스트를 탐색하고, 그 결과를 vim으로 가져오는 방식이 있습니다. 오늘은 vim에서 grep과 같이 여러 파일에서 텍스트를 검색하고 결과를 사용하는 방법에 대해서 살펴보겠습니다.

quickfix

vim에는 에러 리스트 혹은 quickfix 리스트라는 기능이 있습니다. 원래는 make 등의 명령으로 하나 이상의 소스 코드를 컴파일할 때 생기는 에러를 취합하여 목록으로 만들고, 해당 에러가 발생한 파일과 라인을 모두 저장하여 이 목록에서 선택해서 해당 위치로 빠르게 이동할 수 있도록 하는 기능을 제공하는데 사용됩니다. quickfix 목록은 :copen [size] 명령으로 분할창으로 열 수 있으며, 이 때 size 값은 분할창의 크기(높이)를 지정하는데 사용합니다.

:vimgrep, :grep 명령으로 vim에서 내부/외부 탐색을 했을 때의 매치되는 결과 역시 에러 목록과 마찬가지로 해당 단어가 어느 파일의 몇 번째 줄에 있는지를 목록으로 보여주고, 선택했을 때 빠르게 이동할 필요가 있습니다. 이 기능은 quickfix 목록이 제공하는 내용과 완전히 동일하기 때문에 :vimgrep, :grep 명령에서 찾은 결과 역시 quickfix 창으로 표시됩니다.

복잡하게 설명했지만, notepad++ 같은 조금 ‘좋은’ 텍스트 편집기나 코드 편집기들이 이미 제공하고 있는 기능입니다. 그걸 vim에서도 사용할 수 있다는 이야기에요.

vimgrep 사용하기

:vimgrep 명령은 다음과 같이 사용합니다. 패턴 앞뒤의 / 는 다른 기호문자로 변경하는 것이 가능합니다. 그 뒤에 여러 파일이나 glob 패턴등을 사용해서 대상을 지정할 수 있습니다.

:vim[grep]! /{pattern}/[g][j][f] {file...}

몇 가지 옵션이 있어요.

  • g : 한 행에 매치가 여러번 있으면 중복해서 결과에 포함한다.
  • j : 첫번째 결과로 자동으로 점프하지 않게 한다.
  • f : 퍼지 문자열 매칭 기법을 적용하며, 주어진 패턴을 정규식으로 처리하지 않는다.

예를 들어 현재 디렉토리의 및 하위 디렉토리의 모든 파이썬 파일에서 calculdate( 라는 부분을 탐색하려면 다음과 같이 실행하면 됩니다.

:vimgrep /calculate(/ **/*.py

vim은 결과를 찾으면 즉시 첫번째 파일의 위치로 이동합니다. 그리고 이때 하단 상태메시지 영역에서는 (1 of 91): x = calculate(a, b) 와 같이 quickfix 목록에서 현재 이동한 항목의 위치를 표시합니다.

:copen 을 실행하면 quickfix 창을 열 수 있습니다. j/k 키를 사용하여 행을 선택하고 엔터키를 누르면 해당 파일의 위치로 즉시 이동할 수 있어요.

:vimgrep 은 vim이 각각의 파일을 일일이 열어서 내부에서 패턴을 찾고, 찾은 위치의 정보를 quickfix 리스트에 추가합니다. 따라서 만약 적절한 플러그인이 설치되어서 작동하고 있다면, 단순히 디스크 내의 텍스트 파일 외에 네트워크를 통해 원격 컴퓨터의 파일을 탐색하거나, 압축파일 내의 파일에 대해서도 검색이 가능합니다.

quickfix 사용법

  • :copen – quickfix 창을 엽니다.
  • :cclose – quickfix 창을 닫습니다.
  • :cn[ext] – quickfix 목록에서 다음 위치로 점프
  • :cp[revious] – 이전 위치로 점프

:cnext 를 연속으로 입력하는 것이 좀 번거롭다면, 한 번 실행한 후에 노멀보드에서 @: 명령을 사용해서 이전 Ex 명령을 실행할 수 있습니다. (@ 명령 자체는 특정 레지스터의 내용을 실행하는 명령입니다.) 두 번째부터는 @@를 누르는 것만으로도 계속 반복할 수 있습니다.

외부 grep 명령 사용하기

grep은 유닉스 쉘에서 워낙에 많이 쓰이던(!) 도구이기에 vim은 grep과 상호작용할 수 있는 인터페이스를 갖추고 있습니다. grep은 그 자체로 너무 구식인 관계로 지금은 성능이 느리다는 평가를 얻고 있습니다. 그리고 이를 대체하기 위해 작성된 경쟁자들이 많이 존재합니다. ripgrep 이나 ag(silver searcher) 같은 도구들이 있죠. 이러한 경쟁자들은 기존의 grep을 대체하기 위한 목표를 가지고 있기 때문에, vim에서도 grep을 쓰는 대신에 이런 도구들도 대체하여 사용하는 것이 가능합니다.

  • 'grepprg':grep 명령에 사용될 프로그램 경로
  • 'grepformat':grep 명령이 받아온 결과를 파싱하는 서식
:gr[ep][!] {arguements}

:grep 명령으로 전달한 모든 인자는 외부 프로그램의 인자로 그대로 전달됩니다. 성공적으로 탐색된다면 첫번째 결과로 이동하고, quickfix 목록이 작성됩니다.

개인적으로는 “ag”를 grep 대신 사용하고 있습니다. 윈도용으로도 나와있고 성능도 제법 괜찮은 편입니다. 성능 자체는 ripgrep이 더 우수하지만, 대상 파일 경로에 glob 패턴을 사용하려면 굳이 -g **/*.py 와 같은 추가 옵션을 붙여야 하는 점이 좀 불편하기도 해서요. 다음과 같이 설정해주었습니다.

set grepprg=ag\ --vimgrep
set grepformt^=%f:%l:%m

“quickfix” 목록의 파일에 대해 한꺼번에 치환하는 방법

만약 어떤 함수명을 모든 파일에서 한꺼번에 변경하려면 어떻게 해야할까요? :grep/:vimgrep 명령을 사용하여 quickfix 목록에 모아둔 위치를 일일이 방문하여 치환 명령을 내리고, :cnext 명령을 실행하는 과정을 매크로로 녹화한 후에 계속 반복하도록 하는 방법이 있겠습니다.

vim을 실행할 때 여러 파일 이름을 인자로 주게 되면 한꺼번에 이런 파일들을 열게 되는데, 이 때 각각의 파일을 열기만 하는 것이 아니라 “arument list”라는 목록으로 따로 묶어주게 됩니다. 이렇게 하면, :argdo 라는 명령을 사용하여 각각의 파일에 대해 동일한 명령을 한 번의 명령으로 반복할 수 있게 합니다. vim을 재시작하지 않더라도 :args 명령을 사용해서 argument list는 재설정이 가능한데요, 그럼 quickfix 목록에서 파일을 추출해서 argument list를 재설정 하면 되지 않을까요?

vim 내장함수 getqflist()는 현재 quickfix 목록의 정보를 리스트로 반환합니다. 리스트의 각 아이템은 각각의 버퍼, 라인, 행 등등의 정보를 담고 있는 사전(dict) 객체입니다. 따라서 이를 통해 각 버퍼의 파일을 알 수 있습니다. 다음 함수는 quickfix 목록을 가져와서 중복없이 파일의 목록으로 변환하고, 이를 사용하여 다시 argument list를 재설정하는 함수입니다.

# autoload/helper.vim
# written in vim9script

export def QFixList2Args()
  var xs = getqflist()
  if empty(xs)
    return
  endif
  xs = xs->map((i: number, x: dict<any>) =>
      x.bufnr->bufname()->fnameescape())
  execute 'args ' .. (xs->sort()->uniq()->join(' '))
enddef

다음과 같이 Qargs 라는 명령을 만듭니다. 이 명령은 quickfix 목록에 있는 파일들로 argument list를 재설정합니다.

# vim9 script
vim9script
import autoload "helper.vim"
# ...
command -nargs=0 Qargs helper.QFixList2Args()

이제 다음과 같이 특정한 이름을 전체 소스에 대해 변경하는 작업을 할 수 있겠네요. 끝에 |update 를 붙여서 저장까지 합니다. 불안하면 이 부분을 빼고 치환한 다음, 결과를 확인하고 :argsdo update 명령을 따로 사용해도 됩니다.

:grep foo **/*.py
:Qargs
:argdo %s/foo/bar|update

로케이션 리스트 및 qf리스트 조작하기

:grep, :vimgrep, :make 등 qf 리스트를 사용하는 명령들은 qflist 대신에 location list 를 사용하는, 앞에 l 이 추가된 버전의 :lgrep, :lvimgrep, :lmake 등의 명령을 가지고 있습니다. quickfix 목록은 vim 프로세스 하나에 한 개만 존재하지만, location list는 이와 비슷한 기능은 제공하지만 window에 종속되는 특징이 있습니다. 즉, 목록창과 연관되어 있는 창이 닫히면 해당 목록은 사라지게 됩니다.

:vimgrepadd, :grepadd 라는 두 개의 명령이 추가로 존재합니다. :vimgrep, :grep 명령은 실행될 때마다 quickfix 목록을 전부 지우고 새 결과로 교체합니다. :vimgrepadd 는 기존 목록을 유지하면서 새로 검색된 결과를 기존의 quickfix 목록에 추가하게 됩니다. 특정한 조건에 의해서만 quickfix 목록을 지우고 싶다면 setqflist([]) 를 호출해서 qf목록을 빈 리스트로 교체하는 방법이 있습니다.

그리고 이 모든 기능들은 loc리스트에 대해서도 똑같은 버전이 존재합니다. :lvimgrepadd 라든지, getloclist(), setloclist() 같은 함수들도 있습니다. 이들은 다른 플러그인용 기능을 작성할 때 사용될 수 있겠습니다.

예를 들어 버퍼 목록의 모든 버퍼에 대해서 찾기를 수행하고 싶다면, 다음과 같은 함수를 만들어서 사용할 수 있습니다. bufdo 를 사용해서 현재창에서 모든 버퍼를 열어서 :lvimgrepadd 로 찾는 영역을 loc 리스트에 추가합니다. 그런 다음, 결과가 있다면 loc 리스트를 열어서 보여줍니다.

def FindInBufAll(item: string = '')
  var temp = empty(item) ? expand('<cword>') : item
  var pattern = (temp =~ '^/.*/$') ? temp : '/' .. escape(temp, '\/') .. '/'
  setloclist([])
  execute 'bufdo lvimgrepadd ' .. pattern .. ' %'
  if !empty(getloclist()) | lopen 6 | endif
enddef

command FindInAllBuf <SID>FindInBufAll(<q-args>)