2009/08/26

Recent entries from same category

  1. VimConf 2023 Tiny に参加しました
  2. Vim で Go 言語を書くために行った引越し作業 2020年度版
  3. Vim をモダンな IDE に変える LSP の設定
  4. ぼくがかんがえたさいきょうの Vim のこうせい 2019年 年末版
  5. VimConf 2019 を終えて

vimを使っていて人のスクリプトの一部が気に入らない場合、直接書き換える事もするのですが、最近はGLVS(GetLatestVimScripts)を使う事の方が多く、せっかく書き換えたスクリプトを新しいアップデートで上書きされたりして悲しい事になったりします。書き換えて違うファイル名で保存する...なんてのも方法かもしれませんが、いっそvimscriptの中のある関数だけ書き換えられればいいんじゃないか...と思って書き換える方法を考えてみました。
まず、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は違うスクリプトファイルに宣言されていても問題ありません。
イメージはコード内のコメントで分かって頂けると思います。時と場合によっては使えそうな機能ですね。

まぁ、ほとんど使い道ないでしょうが...苦笑
Posted at by | Edit