2012/05/10

Recent entries from same category

  1. Software Design 2018年7月号「Vim 絶対主義」の執筆に参加しました。
  2. Vim の :terminal の中から外の Vim を操る方法
  3. Vim と日本語入力の話。
  4. Windows で子プロセスの標準入出力バッファリングを無効にする
  5. Vim で端末機能が動くようになった。

この記事は、Vim Advent Calendar 2011の記事です。

モテる男のVim script短期集中講座 長文になります。ただし、以下を読んで理解出来たのであれば、きっと貴方もVim scriptで簡単なアプリケーションが書けるレベルになっていると信じています。
Vim scriptをちゃんとマスターして、来年の夏には引き締まったボディでビーチを歩いてみませんか。
貴方のvimrcに皆の視線が釘付けになる事は間違いないでしょう。

あの人が書いたVimプラグイン、どうやって動いてるんだろう。
あの人が書いたVimプラグインと同じ事がやりたい。
そう思った貴方もこれを理解すれば、おおよそ仕組みが分かる程度を記述したつもりです。



まずVim scriptで扱える方は以下の通り
数値let foo = 1
文字列let foo = "bar"
関数リファレンスlet Foo = function("Bar")
リスト(List)let foo = ["foo", "bar"]
辞書(Dictionary)let foo = {"bar": "bar"}
浮動小数点数let foo = 3.14
ここで関数リファレンスのみ格納先の変数の先頭が大文字になっていますが、これはVim script特有の制限になります。
数値は一般的な言語と同じ様に四則演算できます。文字列については.を用いて連結します。 また文字列は添え字によるアクセスが可能で、pythonの様に
echo "hello"[1:3]
という記述が出来ます。その他文字列操作関数については:help evalに詳しく記述されています。
また型同士の比較についてはthincaさんの記事が役立ちます。
Vimスクリプト基礎文法最速マスター - 永遠に未完成

Vimスクリプト基礎文法最速マスター vim 流行ってるみたいなので遅ればせながら便乗。需要?何それおいしいの? Perl基礎文法最速マスター - サンプルコードによるPerl入門 Route 477...

http://d.hatena.ne.jp/thinca/20100201/1265009821
気をつけなければいけないのが、数値については.を用いて文字列と結合し、文字列化することが出来ますが、浮動小数点については特殊で文字列に直す場合にはstring()を使って文字列化する呼び出す必要があります。また文字列からfloatに直す場合においてもstr2float()を使う必要があります。
その他、それぞれの型での操作方法についてもthincaさんの記事が詳しいです。
なお、thincaさんの記事をVimのhelp化した物が提供されています。よろしければインストールしてVimから参照できる様にしておくと便利です。
vim-jp » Learn VimScript

VimScriptは他の言語と比べ癖があり、Vimの仕様と直結している為、他の言語と同等の機能が無かったり、もしくはやり方が異なったりするものも中にはあります。 しかしながらVimScriptはVim...

http://vim-jp.org/2011/11/12/learn_vimscript.html

function


Vim scriptでも関数が定義出来ます。
function Foo()
  echo "foo"
endfunction
この関数をコマンドラインから実行すると
:call Foo()
foo
と表示されます。また同じ関数名を定義するにはfunction!を指定します。
function! Foo()
  echo "foo"
endfunction
さて、よくVimのプラグインを使っていてエラーが発生した際にスタックトレースの様な物がドバーーーと出た事はありませんか?
実はVim scriptはエラーが発生した場合においても処理が続行します。これが他の言語と大きく異なる所です。例えば
function! Foo()
  asdfasdf
  echo "foo"
endfunction

function! Bar()
  call Foo()
  echo "bar"
endfunction

call Bar()
このコードを実行すると(foo.vimという名前で保存し:source foo.vimとして実行します)
Error detected while processing function Bar..Foo:
line 1:
E492: Not an editor command: asdfasdf
foo
bar
と表示されます。つまり不明なコマンドを検出してもfooは表示されますし、barも表示されます。
前述の様にスタックトレースの様な物がドバーーーと出るのはこれが原因です。例えばエラーの出た行が変数宣言および代入文であった場合、エラーによりその変数は定義されなかった物となります。
これにより、以降その変数を参照する処理において全てエラーが発生する事になります。

しかしながらVim scriptにおいても例外処理が無い訳ではありません。
その2つをご紹介します。


try/catch/finally


これは他の言語でも実装されている物と同様です。
上記の例でasdfasdfをtry/catchで囲ってみましょう。
function! Foo()
  try
    asdfasdf
    echo "foo"
  catch
  endtry
endfunction

function! Bar()
  call Foo()
  echo "bar"
endfunction

call Bar()
実行すると
bar
だけ表示されますね。うまくエラーが回避出来た事を意味します。さて、このcatch区で関数をreturnさせても良いのですが、可能ならばエラー情報を引き継いで関数を抜けたいものです。
Vim scriptの場合は他の言語と少し異なる形式になります

abort識別


こんどは以下の様に記述してみましょう。
function! Foo() abort
  asdfasdf
  echo "foo"
endfunction

function! Bar()
  call Foo()
  echo "bar"
endfunction

call Bar()
実行すると
Error detected while processing function Bar..Foo:
line 1:
E492: Not an editor command: asdfasdf
bar
fooが表示されませんでした。abort識別はエラーが発生した際に即時で関数を抜ける事を明示します。
上記のtry/catch/finallyと併用すると
function! Foo() abort
  asdfasdf
  echo "foo"
endfunction

function! Bar()
  try
    call Foo()
    echo "bar"
  catch
    echo "Failure"
  endtry
endfunction

call Bar()
こうする事で例外を山を見る事無くグレイスフルなエラーメッセージが表示される様になります。
なお、try/catch/finallyに加えてthrowも使えます。文字列を指定して例外を投げます。主に自ら例外を投げる場合に使用しますが、先頭がVimから始まる文字列は内部処理の為に予約されています。

スコープ


さて一度ここでスコープの説明をします。Vim scriptには、関数内で有効なローカルスコープl:、スクリプトファイル内で有効なスクリプトスコープs:、グローバル域で有効なグローバルスコープg:が存在し、それ以外にもバッファスコープb:、ウィンドウスコープw:、タブスコープt:が存在します。変数はこれらのスコープに宣言出来ますが、関数はグローバルスコープ、スクリプトスコープに限られます。それ以外で宣言してもエラーは出ませんが、それは単にそういう名前のグローバルスコープ関数が定義された物として扱われます。
さて、このスコープ内関数ですがスコープとは言えど外から呼び出す事が出来ます。外部からはSIDという修飾を付けれる事で呼び出す事が出来ます。
これについては:help <SID>を参照して頂いた方が分かりやすいでしょう。

range識別


abort識別と同じ様にrange識別という物があります。これはVim独特の物になりますが、この識別を指定するとa:というプレフィックスでアクセス出来る引数にa:firstlinea:lastlineという変数が参照出来る様になります。
function! MakeYouVim()
  echo getline('.')
endfunction
この様に宣言し、別のバッファで
:%call g:MakeYouVim()
と実行すると、全ての行で毎回この関数が呼ばれるのですが、range識別を付ける事で行範囲を纏めて1回で呼び出す事が出来る様になります。その際、最初の行番号と最後の行番号を意味するのがa:firstlinea:lastlineです。
function! MakeYouVim() range
  for n in range(a:firstlinea:lastline)
    echo "お前も " . getline(n) . "にしてやろうか!"
  endfor
endfunction
よってこの様にループを回す事も出来ます。

dict識別


Vim scriptで扱える型の一つにDictionaryがありますが、このDictionaryにはメンバ関数を定義する事も出来ます。
let s:foo = {"name""Bar"}

functions:foo.bar()
  echo "my name is " . self.name
endfunction

call s:foo.bar()
この際、s:foo.barは暗黙でdict識別が付けられている事を意味します。dict識別が付いている関数でself変数を使ってDictionary自身を参照する事が出来ます。このdict識別は通常の関数にも指定出来ます。ですので例えば
let s:foo = {"name""Bar"}

functions:foo.bar()
  echo "my name is " . self.name
endfunction

functions:my_bar() dict
  echo "my name is ババア"
endfunction

let s:foo["bar"= function("s:my_bar")

call s:foo.bar()
通常の関数にdict識別を付け、Dictionaryのメソッドを書き換える事で通常と異なる処理を実行出来ます。
これを使う事で、結構いろんな事が出来る様になります。
ちょっと例を示しましょう。
例えばバッファをHTML化するコマンド:TOHtmlは内部ではプログレスバーウィジェットという物を使っているのですが、これのオリジナルが以下の物になります。
progressbar widget - pimp your plugins : vim online

progressbar widget : pimp your plugins

http://www.vim.org/scripts/script.php?script_id=2006
これを使えばVim scriptからプログレスバー(実際にはステータスバーの表示を変えてるだけ)を表示出来る様になります。
let pb = vim#widgets#progressbar#NewSimpleProgressBar("Processing:"100)
for i in range(0100)
  sleep 50ms
  call pb.incr()
endfor
call pb.restore()
このプログレスバーの表示方式を変えてみましょう。オリジナルのソースからs:progressbar.paintを貰ってきてs:progressbar_paintとしてローカルにdict識別付きで宣言しましょう。
scriptencoding utf-8

functions:SID()
  return matchstr(expand('<sfile>')'<SNR>\zs\d\+\ze_SID$')
endfun
let s:sid = s:SID()

let s:str = [
\'(゚Д゚;)',
\'(  ゚Д)',
\'(    ゚)',
\'(     )',
\'(゚;   )',
\'(Д゚; )']

functions:progressbar_paint() dict
  let max_len = winwidth(self.winnr)-1
  let t_len = strlen(self.title)+1+1
  let c_len  = 2*strlen(self.max_value)+1+1+1
  let pb_len = max_len - t_len - c_len - 2 - 7
  let cur_pb_len = (pb_len*self.cur_value)/self.max_value

  let t_color = self.items.title.color
  let b_fcolor = self.items.bar.fillcolor
  let b_color = self.items.bar.color
  let c_color = self.items.counter.color
  let fc= strpart(self.items.bar.fillchar." ",0,1)

  let stl =  "%#".t_color."#%-( ".self.title." %)".
            \"%#".b_color."#|".
            \"%#".b_fcolor."#%-(".repeat(fc,cur_pb_len)."%)".
            \"%#".b_color."#".repeat(" ",pb_len-cur_pb_len).s:str[self.cur_value%len(s:str)]."|".
            \"%=%#".c_color."#%( ".repeat(" ",(strlen(self.max_value) - strlen(self.cur_value))).self.cur_value."/".self.max_value."  %)"
  set laststatus=2
  call setwinvar(self.winnr,"&stl",stl)
  redraw
endfun

let pb = vim#widgets#progressbar#NewSimpleProgressBar("Processing:"100)
let pb['paint'= function(printf('<SNR>%d_progressbar_paint', s:sid))

for i in range(0100)
  sleep 50ms
  call pb.incr()
endfor
call pb.restore()
この様にメソッドを上書きしてあげる事で、アイコンがグルグルまわるプログレスバーが実現出来る様になります。
progressbar
なお、Vim scriptで関数リファレンスを取るにはfunction()関数を用います。
function()で得た関数リファレンスは直接()をつけて呼び出せますが、call関数を使用すると引数を配列で渡して動的に呼び出す事が出来ます。
functions:a(str)
    echo "a:".a:str
endfunction

functions:b(str1, str2)
    echo "b:".a:str1.a:str2
endfunction

let F = function('s:a')
call F("helloworld")

let F = function('s:b')
call call(F, ["hello""world"])

map


一般的なVim scriptはキーマップを提供します。
全てのモードにマップするmap、それぞれのモードにマップするimap(インサートモード)、nmap(ノーマルモード)、cmap(コマンドモード)、vmap(ビジュアル選択モード)、そしてそれらを再帰的にマップさせないinoremapnnoremapcnoremapvnoremapが一般的に使われます。その他幾らかありますが、おそらく使わない事の方が多いでしょう。
マップの実行は以下の様に行います。
nnoremap <buffer> <silent> :call Foo()<cr>
もちろん、インサートモードから関数を実行しようと思えば、:の前に<esc>を入れなければなりません。
<buffer>は現在のバッファのみにマップされ、<silent>は実行する関数でのメッセージ表示を抑制します。
なお、インサートモードにおいて、関数の評価結果から入力文字列を挿入させる為には<expr>指定をつける事で実現出来ます。
inoremap <expr> <c-x> Foo()

command


コマンドモードから実行出来るコマンドを定義します。ユーザが作るコマンドはユーザコマンドと呼ばれ、名前の先頭が大文字である必要があります。
command! Foo :call Foo()
関数に渡す引数の指定や、行の範囲を関数に渡す為の指定がありますが、:help :command-argsに詳しく記述されています。

autoload


さて、最近のVim scriptにはよくfoo#bar#baz()という記述をよく見かけます。これはautoloadとよばれます。これまでVim起動時にはpluginフォルダは以下の.vimファイルがすべて読み込まれていました。これにより、目的の編集作業に必要ない関数や変数の宣言、初期化処理が実行されていました。これでは起動が重くなるだけです。そこで登場したのがautoloadです。autoloadは、autoload/foo/bar/baz.vimの様にフォルダ階層で区切られたファイルが#でセパレートされたネームスペースで呼び出され、call foo#bar#baz#XXX()というアクセスの瞬間にbaz.vimが読み込まれるという仕組みを持っています。
ですので、最近のVim scriptはpluginではコマンドやキーマップのみを宣言するのが最近の流儀です。例えばGistにテキストをポストするgist.vimplugin/gist.vimには実質の処理としては
command-nargs=? -range=% Gist :call gist#Gist(<count><line1><line2><f-args>)
しかありません。

autocmd


Vimの操作に伴い、イベント処理を行う事が出来ます。

autocmd BufNewFile,BufRead *.html call g:MyHtmlSetup()
この例では、*.htmlの様なファイル名としてバッファが作られた、もしくはファイルが読み込まれたタイミングでイベントが発動し、MyHtmlSetupという関数を呼び出す例になっています。
多くのVimプラグインはこれを使ってファイルタイプ独自の処理を行っています。
詳しくは:help :autocmdを参照して下さい。

おまけ1: Vim scriptにはlambdaが無い


Vim scriptにはlambdaがありません。しかしながら使いたくなるのがプログラマです。
現に、Vim scriptのsort関数の第二引数には関数リファレンスを取ります。既述の様に名前を付けて宣言しても良いのですが、lambdaの様に指定出来たら気持ちいいですよね。
しかしながらここで大きな問題があります。Vim scriptには構文を変えられるだけの力量がありませんし、eval関数で関数を宣言する事が出来ません。
前者については言語の仕様なので仕方ありません。諦めましょう。しかしながら後者について、これが何を意味しているのかというと「関数を作ろうと思ったら関数宣言しないと駄目」という事なのです。もちろんVim scriptの:execute命令を使えば関数も定義出来ます。
例えばライブラリとして:execute命令を使って関数を宣言する物を作ったとしましょう。
functiong:MakeLambda(expr)
  exec "function! s:lambda(...)\n"
  \."  return eval(".string(a:expr).")\n"
  \."endfunction\n"
  return function('s:lambda')
endfunction

echo g:MakeLambda("a:1+a:2")(1,2)
もちろんこの関数には欠陥があります。関数リファレンスを返しますが、2回目の呼び出しで1回目の関数を壊してしまいます。ここは以前の余地がありますね。
さて、何が問題か。この関数、実行されるのがg:MakeLambdaを宣言したスコープと同じになってしまうのです。lambdaは通常、実行したスコープと同じスコープでキャプチャされるべき物ですが、これが出来ません。実はVim scriptの:execute命令は実行位置スコープで動作するのでは無く、宣言位置スコープで実行されるのです。
よっていくら工夫しても呼び出し元のスコープでは実行されないのです。この辺は
Big Sky :: あなたはVim scriptを知らない

この記事はsuginoy氏のブログ 杉風呂2.0 - A Lifelog - の記事" あなたはJavaScriptを知らない "をパロったものです。suginoyさんの許可を得て公開します。原文は2...

http://mattn.kaoriya.net/software/vim/20111121114639.htm
Big Sky :: vimでスクリプト内関数を書き換える

vimを使っていて人のスクリプトの一部が気に入らない場合、直接書き換える事もするのですが、最近はGLVS(GetLatestVimScripts)を使う事の方が多く、せっかく書き換えたスクリプトを新し...

http://mattn.kaoriya.net/software/vim/20090826003359.htm
と、thincaさんに間違いを指摘してもらった記事
Re: vimでスクリプト内関数を書き換える - 永遠に未完成

Re: vimでスクリプト内関数を書き換える vim そう簡単に行かないのが Vim の恐ろしいところ。 Big Sky :: vimでスクリプト内関数を書き換える とんでもない落とし穴があります。 ...

http://d.hatena.ne.jp/thinca/20090826/1251258056
を読んでもらえると分かります。つまり、どうあがいてもスコープ指定でeval実行する事は出来ないという事です。Vim scriptにはjavascriptで言うところのwithFunction.bindがありません。
しかしながら先日、Vim scriptでlispエンジンを書いた時に再度実装し気付きました。
要は、スコープ参照したい位置に関数を作れば良いのです。
mattn/lambda-vim - GitHub

generate lambda expression for vim.

https://github.com/mattn/lambda-vim
これを使うには以下の様にします。
exe lambda#setup()

let s:foo = 1
echo lambda#once(s:, 's:foo + a:1')(3)
上記の「:executeは実行スコープ」というのを逆手に取り、lambda#setup()から関数定義の文字列を貰います。そして動的にスクリプトスコープへ.lambda_genという、call s:XXX()の形式では呼び出せない関数を作ります。
この関数が内部でeval()を実行する為、上記の例の様にlambda内からs:fooを参照する事が出来る様になります。
まぁ、最初のlambda#setup()が気持ち悪い気がしますが、幾分便利になった気がします。
なお、このライブラリには毎回個別の関数リファレンスを返すlambda#genと、毎回関数を壊して1度しか使わないlambdaの為の関数生成処理、lambda#onceが用意されています。
さらにそれぞれ、第二引数以降は実行時に評価されるコンテキスト変数として扱う事ができ、
echo lambda#once(s:, '_[0] . (a:1+a:2) . _[1]''foo''bar')(1, 2)
の様に_という名前の配列で参照出来ます。
良かったら使ってみて下さい。

おまけ2: Vim scriptから使えるライブラリ


上記lambdaについてもそうですが、autoloadが導入されて以降、Vim scriptにもライブラリという概念が入ってきました。
有用な物としては
名前 説明
webapi-vimVimからhttp通信を行う際に便利なユーザエージェント、XMLパーサ、HTMLパーサ、JSONパーサ、OAuthライブラリ、Atomライブラリ、XMLRPCライブラリ、などWebのAPIに関する色々な物が入っています。私が開発しています。
wwwrenderer-vimwebapi-vimでHTMLをパースしたDOMからテキストブラウザの様な表現を行う為のライブラリです。私が開発しています。
vital.vimバンドル型のライブラリセットで、ポータビリティ、バージョン管理に優れています。ujihisaさん、Shougoさん、tyruさん、thincaさん、と私が開発しています。
vimprocVimからプロセスを制御する事が出来るライブラリです。コンパイルが必要です。以前は中平さんが開発していた物をShougoさんが引き継いでいます。
vim-remoteVimはプロセス間通信、スレッド間通信、非同期処理が弱いのですが、それをVimのremoteプロトコルを使って通信出来る様にライブラリ化した物です。中平さんが開発しています。
open-browser主にURLの抽出やブラウザの起動に特化したライブラリです。tyruさんが開発しています。
openbufバッファの生死管理やイベントハンドリングに使用します。thincaさんが開発しています。
その他、もっと沢山あって書ききれませんが、日本人が開発していて著名な所だと問い合わせも容易ですし、間違いなく日本語に対応しています。

上達方法


これについて先日、日本人Vimmer達が議論していましたが、ブログやヘルプ、www.vim.orgにあるスクリプトを見て勉強したと言ってました。
まずはコードを読め...と言うのが先人からの教えでしょうか。

まとめ


Vim scriptの関数は他の言語から比べると、かなり取っ付きにくい性質を持っています。
DISられても当然と思われるでしょう。しかしながらこれはエディタが先に生まれ、設定ファイルを記述するべくして生まれ、それがアプリケーションを記述する為に進化していったという過程を考えると、必要最低限な実装と制限であったと言えるでしょう。

今後、Vimがある限りVim scriptは無くならないでしょう。しかしその制限された中でも上記の様なトリッキーな技によって皆を沸かせる事が出来るのも、Vimが持っている性質であるのではないでしょうか。

blog comments powered by Disqus