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

사용자 함수 정의

사용자 함수를 정의하는 명령은 :fu[nction]이다. 뒤에 !을 붙이면 이전에 정의한 함수를 새로 정의하게 된다. 해당 명령 이후에 :endfunction을 만날 때까지 이어지는 명령들은 모두 함수의 본문으로 인식된다.

:function 명령은 함수를 정의하는 것 외에 EX명령으로 모든 함수의 목록을 출력하는 기능을 가지고 있다. 이 때 함수 이름을 인자로 주면 해당 이름의 함수를 출력한다. 또 :function /{pattern}의 형식으로 호출하여 특정한 패턴의 이름을 가진 함수를 출력한다. 예를 들어 File로 끝나는 이름의 함수들은 :function /Files$ 명령으로 출력할 수 있다.

함수 이름

함수의 이름은 통상적인 프로그래밍 언어의 함수 이름 규칙과 비슷하다. 알파벳과 숫자, 언더스코어를 이름에 쓸 수 있다. 단, 내장 함수와의 혼선을 피하기 위해서 사용자 정의 함수의 이름은 대문자로 시작해야 한다. 이는 함수 객체를 참조하는 변수명에서도 동일한 규칙이 적용된다. 변수와 달리 g:b:로는 시작할 수 없다. 함수의 이름은 기본적으로 전역 스코프이며, 유일하게 스크립트 파일 스코프로 s:는 허용된다.

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

사용자 정의 함수를 선언하는 문법은 다음과 같다.

:fu[nction][!] {name}([args]) [range][abort][dict][closure]
...
:endfunc[tion]

인자

인자를 받는 함수는 각 인자의 이름을 ( ... ) 내에 콤마로 분리하여 선언한다. 각각의 인자는 함수의 본체 내에서 a: (argument)를 앞에 붙여야 동작한다. (vimscript는 각 라인별로 해석되는 인터프리터이기 때문에 현재 라인이 함수 내부인지를 실행시점에 판단하기 어렵다.) 인자의 이름은 최대 20개까지 선언가능하며, 그 이후에 ...을 쓸 수 있다.

...으로 선언된 추가 인자들은 a:1, a:2 와 같은 식으로 참조할 수 있다. 이 때 a:0은 이 추가인자들의 개수를 의미한다. 참고로 a:000은 특별히 추가인자 전체를 리스트로 참조할 수 있는 방법이다. (a:1 == a:000[0])

인자는 함수 내에서 변수가 아닌 상수로 취급되며, 다른 값으로 변경할 수 없다. 단, 인자가 리스트나 사전과 같은 컨테이너라면 그 내부의 콘텐츠는 변경이 가능하다. (리스트를 함수에 넘기면 참조로 전달된다.)

범위

[range]를 뒤에 붙여서 선언하는 함수는 줄번호의 범위와 함께 동작할 것으로 기대된다. 특별한 인자 선언이 없었더라도 함수 내부에서 a:firstline, a:lastline으로 범위의 첫줄과 끝줄을 참조하게 된다.

range를 붙이지 않는 함수를 :3,6call Foo()와 같이 호출하는 경우, 각 라인에 대해서 매 함수가 호출된다. 이 때 각 라인을 따라 커서가 이동하기 때문에 함수 내부에서 getline('.')을 호출하면 각각의 라인이 얻어진다. range와 함께 선언된 변수 역시 범위의 시작줄로 커서가 이동하기는 하지만, 함수는 1회만 호출된다.

중지가능

보통 사용자 정의함수 내에서 에러가 발생하면, 에러 메시지가 표시되고 바로 다음 라인으로 넘어가서 실행된다. 어떤 경우에는 함수의 위쪽 코드에서 에러가 발생하여 어떤 변수가 정상적인 값을 갖지 못할 때, 뒤쪽 코드가 그 값을 참조해서 계속해서 에러가 날 수 있다. 이를 방지하기 위해서 함수 선언 시에 뒤쪽에 abort를 명시하면 내부에서 에러가 발생되면, 더 이상 실행하지 않고 중단되도록 한다.

사전에 바인딩

dict와 함께 선언되면 해당 함수는 사전에 바인딩되어 사전의 엔트리로 호출되어야 한다. 내부에서 self를 사용하면 해당 사전을 참조할 수 있다. 이것은 마치 사전을 이용해서 객체의 메소드처럼 사용될 수 있게 한다.

:function! Mylen() dict
:   return len(self.data)
:endfunc

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

이 방식은 별도의 함수 이름을 만들어서 쓰기 때문에 이름공간을 낭비한다는 느낌이다. 다음과 같은 식으로 쓰는 것이 좀 더 깔끔하다.

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

이 문법은 함수의 이름을 숫자로 만들고 mydict.len이 해당 함수를 참조하는 Funcref가 되도록 한다. Funcref는 참조해주는 곳이 없으면 자동으로 해제되므로 사전이 제거된다면 함수 역시 제거될 것이다.

클로저

closure인자는 해당 함수가 클로저처럼 현재 문맥의 값들을 캡쳐하도록 한다. 보통 ‘함수를 리턴하는 함수’를 만들 때 이런걸 쓴다…

:function! Foo()
:  let x = 0
:  function! Bar() closure()
:    let x += 1
:    return x
:  endfunc
:  return funcref('Bar')
:endfunc

함수를 제거하기

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

옵셔널 인자

:function Foo(key, value = 10) 과 같이 인자에 기본값을 정의하는 경우, 호출할 때 인자를 생략할 수 있다. (a = 10, b = 20, c =30) 과 같이 모든 인자를 이름을 붙인 값으로 지정한 경우, (1, v:none, 3)으로 호출하여 b에 대해서만 기본값을 사용하는 것도 가능하다. ... 인자는 역시 옵셔널 인자 뒤에 올 수 있다.

조금 특별한 테크닉으로 앞의 인자 값을 뒤의 인자의 기본값으로 사용하는 방법이 있다. (a = 10, b = a:a)b가 생략된 경우 a의 값으로 사용된다.

여러 값을 리턴하기

vimscript에는 튜플이 없으므로 여러 개의 값을 리턴하려면 리스트나 사전을 사용한다. 사전의 경우 let [a, b] = alist2와 같이 분해할 수 있다.

함수의 호출

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

중괄호 이름

변수를 사용할 때 중괄호 내에 표현식을 쓰는 경우가 있다. 예를 들어 my_{adjective}_variable 이라는 변수표현은 adjective의 값에 따라서 다른 변수를 참조하게 된다. :echo my_{&background}_message 명령은 현재 'background' 설정값에 따라 다른 변수를 참조하는 것이다. 중괄호는 2개 이상 연속될 수 있으며, 중첩되는 것도 가능하다.

이러한 중괄호 이름은 함수명에서도 사용된다. :call 명령에 전달되는 함수 이름은 중괄호 표현을 사용할 수 있다.

함수를 포함하는 플러그인 작성법

" current file is ~/vimfiles/dotfiles/_vimrc.vim
let g:dotfilesdir = expand('%:p:h')

function! ImportVimFile(vfname)
  vfname = substitute(a:vfname, '#', '/', 'g') . '.vim'
  exec 'source ' . vfname
endfunction

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

플러그인으로 사용할 스크립트를 하나 작성한다. 이 스크립트는 함수와 해당 함수를 호출할 수 있는 명령을 정의한다. 이 때 함수의 이름은 반드시 s:를 붙여야 한다. 명령은 반입 후 어디서든 호출할 수 있는 명령이 아니라면 -buffer를 붙여서 해당 스크립트를 반입한 버퍼에서만 호출되도록 한다.

" b.vim
function! s:SayHello()
  echohl CursorLineNr
  echom 'Hello, my name is ' . expand('%:r')
  echohl None
endfunction

command -buffer -nargs=0 SayHello call s:SayHello()

이렇게 하면 s:SayHello의 이름은 실행시마다 달라지기 때문에 명령창에서 call s:SayHello()라고 입력해서 호출은 불가능해진다. 대신 :SayHello로 실행하여 해당 함수를 호출할 수 있다. 만약 키 맵을 사용해서 해당 함수를 호출하고 싶다면 <SID>를 사용한다.

" in b.vim
nnoremap <silent> <leader>sa :call <SID>SayHello()<CR>

이렇게하면 “b.vim” 파일을 반입한 후 leader-s-a를 순서대로 눌렀을 때 해당 스크립트 내 함수가 실행될 수 있다.

사실 많은 플러그인들은 단일 파일이 아닌 형태로 만들어지는데, 이 경우 autoload 디렉토리에 주요한 함수들을 정의하면 해당 내용의 로딩을 호출 시점까지 뒤로 미룰 수 있다. 이는 플러그인으로 인한 로딩 속도 저하를 줄일 수 있고, 이름 충돌 방지 효과까지 있기 때문에 적극 추천한다. 이와 관련된 내용은 시간 날 때 다시 한 번 다뤄보도록 하겠다.