콘텐츠로 건너뛰기
Home » (Vim) 사용자 정의 함수를 작성하는 법

(Vim) 사용자 정의 함수를 작성하는 법

Vim에서 간단한 조작으로 좀 더 복잡한 기능을 만드는 기능으로는 키맵이나 사용자 정의 명령을 정의하는 방법들이 있다. 다만 이러한 기능들은 기본적으로 항상 똑같은 입력을 단축하여 수행하게 하는 수준에서 사용된다. 특정한 환경이나 조건에서 작동하도록 기능을 커스터마이징하고 싶거나, 보다 복잡하고 정교한 기능을 사용하고 싶다면 사용자가 직접 함수를 정의하여 사용할 수 있다.

사용자 정의 함수는 다음과 같이 사용할 수 있다.

  • 특정 키맵을 사용하여 사용자 정의 함수를 호출한다.
  • 사용자 정의 함수를 호출하는 사용자 정의 명령을 만들어 사용한다.
  • 삽입모드 키맵을 만들어서 입력 중에 특정 키를 눌러 사용자 함수를 호출한다.

사용자 함수를 정의하는 전통적인 명령은 :fu[nction] 으로, 명령을 선언한 후 :endfu[nction] 명령을 만날 때까지 입력된 모든 명령이 순서대로 함수의 body로 기록되는 방식이다.이 명령은 새로운 함수를 정의하는 동작 외에도 기존에 생성된 사용자 정의 함수를 모두 리스트업하는 기능을 수행한다. 예를 들어 이름이 “~Files”로 끝나는 사용자 함수들을 확인하려면 :function /Files$ 라고 실행하면 함수의 검색 결과를 볼 수 있다.

함수의 이름과 스코프

함수의 이름을 정하는 규칙은 통상적인 프로그래밍 언어의 함수 이름 규칙과 비슷하다. 사용자 함수 이름은 영어 대문자로 시작하며, 첫글자가 아니라면 알파벳과 숫자, 언더스코어를 이름에 쓸 수 있다.

또한 기본적으로 vimscript에서의 사용자 함수는 전역이다. 하지만 수 많은 플러그인에서도 필요한 기능을 함수로 구현하고 있을 것이기 때문에, 사용자 정의 함수를 전역 스코프에서 정의하는 것은 이름 충돌의 문제를 일으킬 수 있다. 따라서 변수의 스코프와 비슷하게 s:를 앞에 붙여서 해당 스크립트 파일의 범위 내로 이름을 제한하는 방법이 권장되며, 스크립트 범위 내의 함수를 호출하는 방법은 같은 스크립트 내에서 정의한 키맵이나 사용자 정의 명령을 통한 방법이 권장된다.


수많은 플러그인들이 로딩되는 상황에 이름 충돌을 피하기 위해서 s:를 앞에 붙여준다. 이렇게 스크립트 범위로 한정된 함수들은 스크립트 파일이 :source 명령으로 읽어들여질 때, 특별한 이름으로 변경된다. s: 부분이 <SNR>00_ 와 같은 식으로 바뀐다. 이 때 00 부분은 정수값인데 꼭 두자리만 되는 것은 아니다. 이 숫자는 스크립트 파일마다 다르게 부여된다. 따라서 서로 다른 파일에서 혹시나 같은 이름의 함수를 정의하더라도 최대한 충돌을 피할 수 있다.

:fu[nction][!] {name}([{args}]) [range][abort][dict][closure]
# ... 함수 본체
:endfunc[tion]

# e.g.
# 커서의 앞에 지울 글자가 있는지 확인
:function! s:CheckBackSpace()
:   let c = col('.') - 1
:   return c == 0  || getline('.')[c - 1] =~# '\s'
:endfunction

중괄호 이름

vimscript는 동적 변수 이름을 사용할 수 있는데, 예를 들어 my_{adj}_variable 이라는 식으로 변수명 표현을 사용하면 adj 라는 변수의 이름에 따라서 my_cool_variable, my_hot_variable 과 같은 변수 이름으로 대체되어 실행되는 것이다. 중괄호는 변수명 1개에 대해서 여러 개 사용될 수 있고, 중첩되는 것도 지원된다.

함수 명에서도 이러한 중괄호 표현이 가능하다. 물론 함수 정의시에는 불가능하고 :call 명령의 인자로 전달되는 함수 이름을 사용할 때 중괄호 표현을 사용할 수 있다.

함수의 인자

함수가 호출될 때 전달 받을 인자들은 함수 이름 다음에 오는 괄호 속에 콤마로 분리하여 넣어준다.

  • 인자명은 일반적인 변수의 이름 규칙과 동일하며, 보통 소문자를 사용한다.
  • “인자=기본값”의 형태로 인자에 기본값을 지정할 수 있다.
  • 인자는 최대 20개까지 지정할 수 있다.
  • 가변인자를 사용할 수 있다. 인자 위치에 ... 을 사용한다.

인자로 전달된 값들은 함수 내에서 지역변수처럼 사용할 수 있는데, 반드시 앞에 a: 를 붙여야 한다.

:func s:MyFunc1(c, d, e)
:  if a:c == 1               " a:c 를 사용해야 인자값 중 c로 인식한다.
:     return "C = 1"
:  else
:     return "C != 1"
:  endif
:endfunc

가변인자는 이름 없는 리스트 형태로 함수 내부에서 참조할 수 있는데, a:1, a:2 .. 의 순서대로 접근한다. a:0은 가변 인자의 개수를 의미한다. 참고로 a:000은 인자 개수를 제외한 가변 인자 리스트 전체를 의미하게 된다.

인자로 넘겨진 값들은 함수 내에서 상수로 취급되며, 다른 값으로 변경되지 않는다. 다만, 함수로 전달되는 객체들은 모두 참조로 전달되기 때문에 사전이나 리스트 같은 값은 그 내부를 변경하는 것은 허용된다.

기본값을 지정한 인자는 호출시 생략되면 기본값을 사용하게 된다. 또한 인자의 기본값은 위치 상 앞에 오는 인자의 값을 기본값으로 정할 수도 있다.

function! Foo(a, b=2, c=a:a) abort
 ...
 # c는 값이 주어지지 않은 경우 a와 같은 값을 사용하게 된다.
endfunc

범위적용이 가능한 함수

함수를 실행하는 명령은 :call 인데, 이 명령도 :3,44call MyFunc() 와 같은 식으로 버퍼의 라인 번호를 범위로 전달해서 실행하는 것도 가능하다. 이 방법을 통해 범위와 함께 함수를 실행하면, 함수 내부로 암묵적으로 범위를 전달하는 것이 가능한데, 이를 위해서는 함수를 선언할 때 인자 뒤에 range 지시어를 명시해주어야 한다.

범위와 함께 호출된 경우, 줄 범위는 a:firstline, a:lastline 으로 범위의 시작과 끝 줄 번호를 참조할 수 있다.

만약 range 키워드 없이 선언된 함수를 범위와 함께 호출하는 경우에는 매 라인에 대해서 함수를 호출한 것과 같다. 이 경우 함수 내부에서 getline('.') 과 같이 현재 행을 참조하는 함수를 사용하면 각각의 라인을 얻을 수 있다.

실행 중지 옵션

vimscript의 함수 내부의 특정한 행에서 에러가 발생하면, 기본적으로 에러 메시지를 표시한 후 다음 라인으로 실행이 계속된다. 이 경우 에러로 인해 실행이 실패한 라인 때문에 후속 라인에서 에러가 반복적으로 발생할 수 있는데, 이를 방지하기 위해서 함수 선언 시에 뒤에 abort를 붙이면 내부에서 에러가 발생하면 더 이상 실행되지 않고 중단할 수 있다. 디버깅시에 편리하므로 왠만하면 기본적으로 쓰는 것이 좋다.

사전에 바인딩된 함수

dict 키워드를 사용하여 선언된 함수는 특정한 사전에 바인딩되어, 마치 객체의 메소드처럼 사용될 수 있다. 함수 내부에서는 self 라는 키워드를 사용하여 바인딩된 사전을 참조할 수 있다. 이렇게 선언한 함수를 사전 객체의 메소드처럼 사용하려면 사전을 정의할 때 해당 함수를 포함하여 정의해야 한다.

function! MyLen() dict
    return len(self.data)
endfunction

let mydict = #{data: [0, 1, 2, 3], len: function('MyLen')}
echo mydict.len()

실제로는 dict를 사용하기 보다는 dictname.funcname 의 형식으로 바로 정의할 수도 있다. 여러 사전이 공통적으로 함수를 가지지 않는 상황이라면 이렇게 사용하는 방법도 있다.

let mydict = #{data: [0,1,2,3]}
function! mydict.len()
    return len(self.data)
endfunction

echo mydict.len()

위 코드는 실제 정의되는 함수는 그 이름이 임의의 숫자로 결정되고, 해당 함수에 대한 참조를 사전이 가지게 된다. Funcref 객체는 참조수가 0이 되면 자동으로 해제되므로, 사전이 제거되면 함수 역시 제거될 것이다.

클로저

closure인자는 함수를 리턴하는 함수를 작성할 때 사용하는데, 함수가 정의된 영역의 지역 변수들을 캡쳐하게 된다.

function! Foo()
  let x = 0
  function! Bar() closure  # <- Foo.x를 캡쳐한다. 
                           # 호출할 때마다 1증가한 값을 리턴한다.
    let x += 1
    return x
  endfunc
  return funcref('Bar')
endfunc

함수를 제거하기

:delf[unction] 명령은 함수를 제거할 수 있다. !를 붙이면 해당 함수가 존재하지 않아도 에러 없이 수행된다.

여러 값을 리턴하는 함수

Vimscript는 리스트 분해 문법을 지원한다. let [a, b] = MyFoo() 와 같이 여러 값을 한 번에 리턴해야 하는 경우가 있다면 리턴 값들을 리스트에 담아서 리턴하면 된다.

함수의 호출

:call {함수이름}([{인자}])의 형태로 호출할 수 있다. call 앞에는 라인범위를 붙여서 호출할 수 있다.


플러그인에서의 함수

함수의 기본 스코프는 전역 범위이고, 이름 충돌을 피하기 위해서 스크립트 스코프의 함수를 정의하여 사용하는 것이 강력하게 권장된다고 하였다. 특히 플러그인을 작성하려는 사람은 자신의 플러그인을 사용하는 사용자가 어떤 이름을 가진 함수들을 정의할 것인지 알 수 없기 때문에 필수적으로 스크립트 범위의 함수를 작성해야 할 것이다.

스크립트 범위의 함수는 기본적으로 함수가 작성되어 있는 스크립트 파일 내에서만 보일텐데, 어떻게 vim에서 이를 호출하고 사용할 수 있을까? 그 방법은 실제 :call 명령보다는 스크립트 내에서 사용자 정의 명령이나, 플러그인 키맵을 정의하고, 거기서 스크립트 범위 함수를 호출하는 방식을 사용하는 것이다. 이 두 가지 방법에서 함수를 호출하는 방식은 차이가 존재한다.

사용자 정의 명령으로 스크립트 로컬 함수 실행하기

사용자 정의 명령을 만들면, 명령의 이름은 전역 범위에 노출되지만 명령이 실행되는 맥락은 해당 명령이 정의된 스크립트 범위가 된다. 따라서 사용자 정의 명령의 본체 부분에서는 스크립트 로컬 함수를 그대로 호출하는 것처럼 사용하면 되며, 이는 추후에 다른 위치에서 플러그인파일(스크립트 파일)을 로딩하기만 하면 어느 위치에서나 함수를 실행할 수 있다.

let s:rootdir = expand('%:p:h')  "현재 파일의 상위(=현재 파일이 있는 디렉토리)

" 'support#myscript' 를 인자로 받으면
" support/myscript.vim 스크립트 파일을 로딩함
function! ImportVimFile(vfname) abort
    " vfname과 a:vfname 은 별개의 변수
    " vfname은 함수 내의 지역변수
    let vfname = s:rootdir  ..  substitude(a:vfname, '#', '/', 'g') .. '.vim'
    exec 'source '.. vfname
endfunction

command -nargs=1 ImportVim call ImportVimFile(<q-args>)

위 함수는 현재 파일 내의 다른 스크립트나 서브 디렉토리 내의 스크립트를 이름으로 받아서 로딩하는 기능을 구현한 것이다. 일단 해당 파일이 vim에 로딩된 후에는 다른 컨텍스트에서 :ImportVim aaa#bbb 와 같은 식으로 명령을 호출할 수 있다. 이 명령은 자신이 정의된 위치의 컨텍스트에서 실행되기 때문에 명령이 호출되는 위치와 관계없이 함수의 호출이 가능하다.

키 맵을 사용하여 함수 호출하기

특정 키 시퀀스를 입력했을 때 함수가 실행될 수 있도록 할 수 있다. 명령 모드에서 특정 함수의 호출 명령을 입력하고 엔터키까지 입력되는 키 맵을 정의하는 방식으로 키 맵을 사용해서 함수를 호출할 수 있다. 그런데 키 맵의 경우, 해당 키 시퀀스가 입력된 시점에서 키보드로 누르는 키들을 맵핑하는 방식으로 작동하기 때문에, 일반적인 키 맵을 사용해서는 스크립트-로컬 스코프의 함수를 호출하는 것은 작동하지 않을 것이다.

이러한 경우를 위해서 <SID> 라는 특별한 키 맵을 사용한다. 스크립트 범위에서 정의된 함수는 Vim 내부적으로는 <SNR> 이라는 키보드로 입력할 수 없는 키와 해당 스크립트의 고유번호(파일을 로딩할 때 무작위로 부여되는 듯하다.)가 함수 이름 앞에 접두어로 붙게 된다. 이 값은 vim이 실행되는 환경에 따라 달라질 수 있기에 미리 알 수 없지만, <SID> 라는 키가 실제로 해당 스크립트 번호 접두어로 변경된다. 따라서 이를 이용하면, 키 맵을 통해서 사용할 수 있게 된다.

nnoremap  fj  <Cmd>call <SID>MyFunction()<CR>
"  <Cmd> =>  ':<C-u>'처럼 명령모드에서 입력을 시작하는 것을 보장함
"  <SID> =>  해당 스크립트 로컬 함수의 접두어

이렇게하면 실제로 fj를 입력했을 때에는 :call <SNR>12312_MyFunction()<CR> 과 같은 명령이 실제로 실행된다. (이 때 <SNR>은 5글자짜리 단어가 아니라 키보드로 입력 불가능한 별도의 키를 의미하며, 숫자는 그때그때 달라질 수 있다.) 스크립트 식별자를 동적으로 붙여주는 방식을 사용하여 키 맵에서도 해당 함수를 바로 호출하는 셈이다.

플러그인 키맵

대신에 플러그인 파일에서 키맵을 지정해버리는 것은, 함수 이름 충돌을 피하기 위해서 키맵을 충돌시키는 더 나쁜 결과를 가져오게 된다. 플러그인을 사용하려는 사용자 입장에서는 자신이 편한 키맵을 사용해서 플러그인 기능을 호출하고 싶은데, 플러그인이 강제하는 키맵만 사용하는 것은 썩 내키지 않는 결정이다. 뿐만 아니라 이 방식은 플러그인끼리 키 맵핑이 충돌하는 심각한 문제를 야기할 수 있다.

이러한 키 맵 충돌을 피하기 위해서 사용할 수 있는 것이 플러그인 키 맵이다. 앞서 키 맵에서 <SNR>은 스크립트 이름 접두어를 만드는데 필요한 문자로, 키보드로 입력할 수 없는 문자값을 사용한다고 했다. 이와 비슷하게 <Plug> 라는 역시나, 키보드로 입력할 수 없는 키를 사용해서 키 맵을 정의하는 것이다.

그런 후 사용자는 자신이 원하는 키 시퀀스를 해당 키 맵으로 맵핑한다. 그러면 간접적으로 사용자가 입력한 키 시퀀스가 “실제로는 입력이 불가능한 키 시퀀스”로 변경되고, 이것이 다시 실제 작동을 위한 키 시퀀스로 변환되어 vim에 전달되는 것이다. 아래와 같이 플러그인 파일과 vimrc 파일에서 각각의 역할에 맞는 키 맵을 정의하고, 이를 통해서 간접적으로 스크립트 내의 함수를 호출하는 예를 설명한다.

" 스크립트 파일

" 로컬 함수
function! s:MyFunc() abort
  ...
endfunction
" 로컬 함수를 호출하는 플러그인 키맵 정의
noremap <script><silent> <Plug>(MyPlugin-MyFunc)  <Cmd>call <SID>MyFunc()<CR>


" ---------------------------------------------------------------------------
" .vimrc 파일 내에서
" fg 를 누르면 플러그인 키맵을 통해서 플러그인의 함수를 호출한다.
nmap fg <Plug>(MyPlugin-MyFunc)