vim에서 패턴에 매칭되는 영역을 추출하기

vim에서 패턴에 매칭되는 부분을 변경하는 것이 아니라, 매칭되는 부분만 남기고 나머지를 제거하는 방법

vim에서 패턴에 매칭되는 영역을 추출하기
Photo by Nick Harsell / Unsplash

아래 글에서는 notepad++에서 Mark 기능을 사용해서 특정 패턴에 매치되는 부분만을 추출하는 방법을 소개했습니다.

Notepad++ - 특정 패턴의 단어나 문구만 추출하여 정리하기
최근에 텍스트로 된 CSV, JSON 파일과 엑셀을 모두 사용하면서 지난한 작업을 많이 하고 있는데, 다시금 Notepad++의 덕을 많이 보고 있습니다. 단순히 데이터를 포맷팅하거나, 단순한 찾기/바꾸기 외에도 제품바코드 번호나 주문번호 같은 특정 규격의 정보를 큰 파일에서 추출한다거나, 엑셀 파일에서 칼럼을 복사해와서 컴마로 연결하거나 하는 일을 주로 하게 됩니다. 오늘은

그렇다면 vim에서는 이와 같은 작업을 할 수 있는 방법이 있을까요?

:substitute

:s 명령은 버퍼 내에서 특정한 패턴에 매칭하는 부분들을 치환하여 변경하는 명령입니다. 특정 패턴을 다른 고정된 단어나, 혹은 패턴 내 그룹의 배치를 변경하는 식으로 포맷을 바꿀 수 있죠.

" 전체 문서에서 'apple'을 'banana'로 치환
:%s/apple/banana/g

" 11/09/24의 패턴을 2024-09-11로 변경
:%s#\(\d\{2\}\)/\(\d\{2\}\)/\(\d\{2\}\)#20\3-\2-\1/g

치환 명령에서 '바꿀 패턴'에 해당하는 부분은 고정된 문자열이거나, 패턴 내에 그룹이 포함된다면 그룹을 가리키는 패턴이 들어가는 정도입니다.

💡
치환 명령에서 백슬래시가 너무 정신없을 때에는 패턴의 앞에 \v를 써서 very magic 모드를 사용하면 정신건강에 좋습니다.
:%s#\v(\d{2})/(\d{2})/(\d{2})#20\3-\2-\1#g

만약 특정한 패턴에 맞는 단어를 찾아서 그 단어를 대문자로 변환하여 치환하는 것은 어떻게 찾기/바꾸기 기능으로 구현할 수 있을까요? 일반적인 편집기의 찾기/바꾸기 기능은 매치된 단어에 대한 추가적인 연산을 허용하지 않기 때문에, 순수하게 치환 기능만으로는 이를 구현하기 어렵습니다. 보통은 별도의 플러그인이나 아니면 파이썬과 같은 프로그래밍의 영역으로 넘어가는 문제입니다.

하지만 vim은 vimscript라는 스크립트 언어를 내장하고 있고, 이 언어에서 기본적인 문자열 처리를 할 수 있기 때문에, vim에서는 구현할 수 있습니다.

\= 으로 시작하는 치환패턴

"..m"으로 끝나는 단어를 모두 대문자로 만들기

:%s/\v\s(\w+m)\s/\=' ' .. toupper(submatch(1)) .. ' '/g

치환 패턴부분이 \=로 시작하게 되면 vim은 치환 패턴을 vimscript 표현식으로 평가하게 됩니다. 치환해야 하는 문자열은 이 표현식이 리턴하는 문자열 값이 됩니다.

:s 명령의 치환 패턴은 각각의 매치가 발생할 때마다 평가되므로, 이 위치에서 패턴에 매치한 내용들을 어딘가에 모아두었다가 새로운 버퍼에 각 라인별로 붙여넣어주면 되지 않을까요?

이 시나리오를 vim 명령으로 구현해보면 다음과 같습니다.

:let @a = '' | %s/\v\s(\w+m)\s/\=setreg('A', submatch(1), 'l')/gn |\
  new | put! A

해설

setreg(레지스터, 문자열, 옵션)함수는 지정한 레지스터에 문자열을 기록하는 함수입니다. 만약 레지스터 이름이 대문자로 주어지면 write 모드 대신 append 모드가 됩니다. 그리고 옵션의 l은 'line-wise'를 의미합니다. 즉 덧붙이는 각각의 문자열은 개행으로 구분한다는 의미입니다.

submatch() 함수는 좀 특별한 함수로, :substitute 명령의 치환패턴 내에서만 작동하는 함수입니다. 이 함수는 패턴 매칭된 결과에 해당하는 문자열을 반환합니다. 파라미터로는 0부터 시작하는 숫자를 받는데, 눈치 빠른 분들은 알아차리셨겠지만, 0은 매칭된 문자열 전체, 1, 2, .. 부터는 각각의 그룹에 해당하는 내용을 의미합니다.

그리고 :s 함수의 마지막 옵션에서 n 이 붙어있는데, 이 옵션은 몇 개의 라인에서 몇 개의 매치를 찾았는지만 보고하고 실제로는 치환을 실행하지 않습니다. (어차피 setreg() 함수는 전달한 문자열을 그대로 리턴하기 때문에 내용 자체는 변하지 않습니다.)

응용

이 방법을 사용자 정의 명령으로 정의해두고 사용하면 편할 것 같습니다. 아래에는 먼저 vim의 검색 명령(/)으로 검색했던 패턴에 매치되는 모든 문자열을 각 라인별로 새로운 버퍼에 붙여주는 명령입니다.

vimscript9

command -nargs=0 ExtractSearch ExtractSearchPattern()

def ExtractSearchPattern()
  if @/ == ''
    return
  endif
  var temp = @a
  @a = ''
  # :s 에서 검색패턴이 없으면 마지막 검색패턴을 사용
  silent :%s//\=setreg('A', submatch(0), 'al')/gn
  if @a == ''
    return
  endif
  new
  put! a
  normal! gg
  nohls
  @a = temp
enddef

검색 패턴을 같이 입력받는 방법도 있겠지만, 개인적으로는 먼저 검색해서 매치되는 부분이 있는지 확인한 후에 이 명령만 실행해서 추출하는 방법도 좋을 것 같습니다.