Vimscript 배우기 cho.books
제 8 장 · 원서 §28–34

execute · normal · 정규표현식 — Grep 연산자 사례 연구

문자열을 명령으로 실행하고, 노멀 모드 키 입력을 흉내 내고, 정규표현식을 길들인다. 그리고 이 모든 것을 모아 — 진짜 Vim 연산자를 처음부터 만든다.

execute normal! very magic operatorfunc 사례 연구

Section 01execute — 문자열을 명령으로

execute 명령은 문자열을 마치 Vimscript 명령인 것처럼 평가합니다. 임의의 문자열로 명령을 조립할 수 있게 해주는 매우 강력한 도구입니다.

vimscript — execute 기초
:execute "echom 'Hello, world!'"
:execute "rightbelow vsplit " . bufname("#")

둘째 줄은 "rightbelow vsplit "bufname("#")의 결과(이전 버퍼 경로)를 점 연산자로 이어 명령 문자열을 만들고, 그것을 실행합니다.

직관 · execute는 위험한가? 대부분의 언어에서 "eval"류 구문은 눈총을 받지만, Vimscript의 execute는 다릅니다. ① Vimscript 코드는 보통 단 한 사람 — 사용자 자신 — 의 입력만 받습니다. ② Vimscript 문법이 까다로워서, execute가 무언가를 해내는 가장 쉽고 직접적인 길일 때가 많습니다.

Section 02normal! — 키 입력 흉내

normal 명령은 일련의 키를 받아, 그것이 노멀 모드에서 입력된 것처럼 동작하게 합니다. 플러그인을 쓸 때는 항상 normal!(느낌표 붙은 형태)을 쓰세요 — 사용자가 어떤 키를 매핑했을지 신뢰할 수 없기 때문입니다.

vimscript — normal!
:normal! ggdd        " 첫 줄로 가서 삭제
함정 normal!<cr> 같은 특수 문자를 인식하지 못합니다. :normal! /foo<cr>는 글자 그대로 "f, o, o, <, c, r, >"를 검색하려 듭니다. 검색을 실행하는 엔터가 전달되지 않습니다.

해결책은 executenormal!을 결합하는 것입니다. 이것은 Vimscript의 핵심 관용구입니다:

vimscript — execute "normal! ..." 관용구
:execute "normal! gg/foo\\<cr\>dd"

execute가 받는 문자열에서는 \<cr> 같은 이스케이프 시퀀스가 진짜 캐리지 리턴 문자로 변환됩니다. 이 줄은 "파일 맨 위로 → foo 검색 → 검색 실행 → 그 줄 삭제"를 수행합니다. 이 관용구로, 바닐라 Vim에서 할 줄 아는 모든 것을 Vimscript 코드 안에서 할 수 있습니다.

모션 · execute "normal! ..." 의 흐름 01 / 5
스페이스: 재생 · ← →: 단계 이동

Section 03정규표현식과 very magic

Vim은 텍스트 편집기이므로 Vimscript 코드의 상당 부분이 텍스트를 다룹니다. Vim은 강력한 정규표현식을 지원하지만, 별난 점이 있습니다 — Vim에는 정규표현식 파싱 모드가 네 가지나 있습니다!

기본 모드에서는 + 앞에 백슬래시가 필요합니다(\+가 "하나 이상"). 이 혼란을 피하는 가장 제정신인 해법 — 모든 정규표현식을 \v로 시작하세요:

vimscript — very magic 모드
:execute "normal! gg" . '/\vfor .+ in .+:' . "\\<cr\>"
철칙 모든 정규표현식을 \v("very magic")로 시작하세요. 그러면 Vim의 다른 세 가지 정신 나간 정규표현식 모드를 신경 쓸 필요가 없습니다 — \v 모드는 Python·Perl·Ruby에서 익숙한 것과 거의 같습니다.

:set hlsearch incsearch로 검색 강조를 켜 두면 정규표현식을 실험하기 좋습니다. hlsearch는 모든 일치를 강조하고, incsearch는 입력하는 동안 다음 일치를 강조합니다.

Section 04사례 연구 — Grep 연산자

이제 배운 모든 것을 모아 진짜 Vim 연산자를 만듭니다 — "grep 연산자". 어떤 모션이든 받아서, 그 텍스트를 외부 grep으로 검색하는 연산자입니다. <leader>giw는 "단어 안쪽을 grep", <leader>gi'는 "작은따옴표 문자열 안쪽을 grep".

새 연산자는 함수매핑 두 부분으로 시작합니다:

vimscript — grep-operator.vim · 골격
nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

function! GrepOperator(type)
  echom a:type
endfunction

operatorfunc 옵션을 함수로 설정한 뒤 g@를 실행하면, Vim이 그 함수를 연산자처럼 호출합니다. 함수의 type 인자는 어떤 종류의 모션이 쓰였는지 알려줍니다(v, V, char, line).

이제 함수가 실제로 텍스트를 복사하고, 이스케이프하고, grep을 실행하게 채워 봅시다:

vimscript — GrepOperator 완성
function! GrepOperator(type)
  if a:type ==# 'v'
    normal! `<v`>y
  elseif a:type ==# 'char'
    normal! `[v`]y
  else
    return
  endif
  silent execute "grep! -R " . shellescape(@@) . " ."
  copen
endfunction
개념 연산자가 동작하는 단계

① 모션 타입을 검사한다(v=비주얼, char=문자 모션).

② 해당 범위를 마크(`<, `[ 등)로 비주얼 선택하고 y로 복사한다.

③ 복사된 텍스트는 무명 레지스터 @@에 들어간다.

shellescape()로 셸 인자를 안전하게 이스케이프한 뒤, silent execute로 grep을 실행하고 copen으로 결과 창을 연다.

Section 05배려하는 Vimscript 시민

연산자는 잘 동작하지만, Vimscript를 쓴다는 것의 일부는 사용자를 배려하는 것입니다. 두 가지를 더 합니다.

레지스터 보존

무명 레지스터에 텍스트를 yank하면 그 안에 있던 것이 파괴됩니다. yank 전에 저장하고, 끝나면 복원하세요:

vimscript — 레지스터 저장과 복원
function! GrepOperator(type)
  let saved_unnamed_register = @@
  " ... 텍스트 복사, grep 실행 ...
  let @@ = saved_unnamed_register
endfunction

네임스페이싱

스크립트가 전역 네임스페이스에 GrepOperator를 만들면 충돌 위험이 있습니다. 함수 이름을 s:로 시작해 스크립트-로컬로 만들고, 매핑에서는 <SID>를 붙여 그 함수를 찾게 합니다:

vimscript — 스크립트-로컬 네임스페이스
nnoremap <leader>g :set operatorfunc=<SID>GrepOperator<cr>g@
function! s:GrepOperator(type)
  " ...
endfunction
기억하세요 Vim 플러그인을 쓸 때는 항상 코드가 수정하는 설정·레지스터를 저장하고 복원하세요. 그리고 함수는 s:로 스크립트-로컬로 만들어 전역 네임스페이스를 오염시키지 마세요. 이것이 사용자를 놀라게 하지 않는, 배려하는 Vimscript 시민의 자세입니다.

Section 06연습문제

  • :help execute, :help :normal, :help silent를 읽으세요.
  • :help magic:help pattern-overview를 읽고, Vim 정규표현식이 지원하는 것들을 보세요.
  • :help expr-quote를 읽고, 문자열에서 쓸 수 있는 이스케이프 시퀀스를 확인하세요.
  • 완성한 grep 연산자를 ~/.vim/plugin/grep-operator.vim에 두고 실제로 써 보세요.
  • :help operatorfunc, :help map-operator, :help visualmode(), :help <SID>를 읽으세요.
  • :help :grep, :help cnext, :help cprevious를 읽고, 결과 사이를 빠르게 오가는 매핑을 만드세요.