まず、vimにはグローバルスコープ、スクリプトスコープ、ローカルスコープとあり、スクリプトスコープとは1スクリプトファイル内で実行される関数と変数群に位置します。
通常、スクリプトスコープ内の関数には<SID>というスクリプトID識別が付与され、ファイル単位でユニークに格納されています。
ただ実際には識別子が付いているだけで、外部から参照も出来ますしfunction()で関数リファレンスを取る事も出来ます。スクリプトファイルに対する<SID>は
:scriptnames
で一覧出来ますから、この出力をredirで盗み取ってしまえばいいのです。ちなみにウチのubuntuで:scriptnamesを実行した結果は以下の通り。
1: /usr/share/vim/vimrc
2: /usr/share/vim/vim72/debian.vim
3: /usr/share/vim/vim72/syntax/syntax.vim
4: /usr/share/vim/vim72/syntax/synload.vim
5: /usr/share/vim/vim72/syntax/syncolor.vim
6: /usr/share/vim/vim72/filetype.vim
7: /home/mattn/.vimrc
8: /usr/share/vim/vim72/syntax/nosyntax.vim
9: /usr/share/vim/vim72/ftplugin.vim
10: /usr/share/vim/vim72/indent.vim
11: /home/mattn/.vim/plugin/autodate.vim
12: /home/mattn/.vim/plugin/calendar.vim
13: /home/mattn/.vim/plugin/codepad.vim
...
この左数字部分がIDです。これをsplitとmapを使い、スクリプトファイル名から<SID>を得られるハッシュマップ(vim語ではDictionary)に変換します。
function! GetScriptID(fname)
let snlist = ''
redir => snlist
silent! scriptnames
redir END
let smap = {}
let mx = '^\s*\(\d\+\):\s*\(.*\)$'
for line in split(snlist, "\n")
let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
endfor
return smap[tolower(a:fname)]
endfunction
ちなみに、tolowerしてるのはほぼWindows用で、ちょっとした大文字小文字の違いで<SID>が取れなくなって悲しくならない為のおまじないです。次に書き換えるべき関数リファレンスを取得します。<SID>内で定義される関数リファレンスは
<SNR>SID_foo
という識別になります。これをfunction()関数に渡してあげれば関数リファレンスが取得出来ます。これだけでも、実はスクリプトスコープ内の関数を外部から呼び出せてウマーなのですが、ここから本題。
関数を書き換えるにはfunction!と「!」付きで宣言すれば良いのですが引数が決められません。書き換え関数の引数を外部から指定して貰っても良いのですが面倒ですよね。書き換え前の関数引数と書き換え後の関数引数が同じである事は書き換えた側の責任でもありますし、call()関数を使えば引数を配列として呼び出す事も出来ます。そこで以下の様なトリックを使いました。
function! funcA(...)
return call('funcB', a:000)
endfunction
a:000とは可変個引数を宣言した際に使える引数リストです。受け取った引数リストをcall()関数に渡しています。後はこれを動的に実行してやれば書き換え関数の完成です。全体のコードだと以下の様になりました。
function! GetScriptID(fname)
let snlist = ''
redir => snlist
silent! scriptnames
redir END
let smap = {}
let mx = '^\s*\(\d\+\):\s*\(.*\)$'
for line in split(snlist, "\n")
let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
endfor
return smap[tolower(a:fname)]
endfunction
function! GetFunc(fname, funcname)
let sid = GetScriptID(a:fname)
return function("<SNR>".sid."_".a:funcname)
endfunction
function! HookFunc(funcA, funcB)
if type(a:funcA) == 2
let funcA = substitute(string(a:funcA), "^function('\\(.*\\)')$", '\1', '')
else
let funcA = a:funcA
endif
if type(a:funcB) == 2
let funcB = substitute(string(a:funcB), "^function('\\(.*\\)')$", '\1', '')
else
let funcB = a:funcB
endif
let oldfunc = ''
redir => oldfunc
silent! exec "function ".funcA
redir END
let g:hoge = oldfunc
exec "function! ".funcA."(...)\nreturn call('" . funcB . "', a:000)\nendfunction"
endfunction
さて、このスクリプトの使い方。例えば引数で与えられた文字列XXXを使い「Hello: XXX」を表示するスクリプト内関数s:funcAがあったとして、これを「GoodNight: XXX」と表示するスクリプト内関数s:funcBに書き換えたいとします。それぞれ関数リファレンスを得て書き換えたいタイミングでHookFuncを呼べば、それ以降は書き換えられた関数が実行されます。
so hookfunc.vim
function! s:foo(text)
echo "Hello, " . a:text
endfunction
function! s:bar(text)
echo "GoodNight, " . a:text
endfunction
call s:foo("World")
" => Hello, World
"
call s:bar("World")
" => GoodNight, World
" foo を bar に書き換える
call HookFunc(GetFunc(expand("%:p"), "foo"), GetFunc(expand("%:p"), "bar"))
call s:foo("World")
" => GoodNight, World
ちなみにfuncAとfuncBは違うスクリプトファイルに宣言されていても問題ありません。イメージはコード内のコメントで分かって頂けると思います。時と場合によっては使えそうな機能ですね。
まぁ、ほとんど使い道ないでしょうが...苦笑