この記事は、
Vim Advent Calendar 2011の記事です。
長文になります。ただし、以下を読んで理解出来たのであれば、きっと貴方も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
を参照して頂いた方が分かりやすいでしょう。
range識別
abort識別と同じ様にrange識別という物があります。これはVim独特の物になりますが、この識別を指定すると
a:
というプレフィックスでアクセス出来る引数に
a:firstline
と
a:lastline
という変数が参照出来る様になります。
function! MakeYouVim()
echo getline('.')
endfunction
この様に宣言し、別のバッファで
:%call g:MakeYouVim()
と実行すると、全ての行で毎回この関数が呼ばれるのですが、range識別を付ける事で行範囲を纏めて1回で呼び出す事が出来る様になります。その際、最初の行番号と最後の行番号を意味するのが
a:firstline
と
a:lastline
です。
function! MakeYouVim() range
for n in range(a:firstline, a:lastline)
echo "お前も " . getline(n) . "にしてやろうか!"
endfor
endfunction
よってこの様にループを回す事も出来ます。
dict識別
Vim scriptで扱える型の一つにDictionaryがありますが、このDictionaryにはメンバ関数を定義する事も出来ます。
let s:foo = {"name": "Bar"}
function! s: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"}
function! s:foo.bar()
echo "my name is " . self.name
endfunction
function! s: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(0, 100)
sleep 50ms
call pb.incr()
endfor
call pb.restore()
このプログレスバーの表示方式を変えてみましょう。オリジナルのソースから
s:progressbar.paint
を貰ってきて
s:progressbar_paint
としてローカルにdict識別付きで宣言しましょう。
scriptencoding utf-8
function! s:SID()
return matchstr(expand(''), '\zs\d\+\ze_SID$')
endfun
let s:sid = s:SID()
let s:str = [
\'(゚Д゚;)',
\'( ゚Д)',
\'( ゚)',
\'( )',
\'(゚; )',
\'(Д゚; )']
function! s: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('%d_progressbar_paint', s:sid))
for i in range(0, 100)
sleep 50ms
call pb.incr()
endfor
call pb.restore()
この様にメソッドを上書きしてあげる事で、アイコンがグルグルまわるプログレスバーが実現出来る様になります。
なお、Vim scriptで関数リファレンスを取るには
function()
関数を用います。
function()
で得た関数リファレンスは直接
()
をつけて呼び出せますが、
call
関数を使用すると引数を配列で渡して動的に呼び出す事が出来ます。
function! s:a(str)
echo "a:".a:str
endfunction
function! s: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
(ビジュアル選択モード)、そしてそれらを再帰的にマップさせない
inoremap
、
nnoremap
、
cnoremap
、
vnoremap
が一般的に使われます。その他幾らかありますが、おそらく使わない事の方が多いでしょう。
マップの実行は以下の様に行います。
nnoremap <buffer> <silent> :call Foo()<cr>
もちろん、インサートモードから関数を実行しようと思えば、
:
の前に
を入れなければなりません。
は現在のバッファのみにマップされ、
は実行する関数でのメッセージ表示を抑制します。
なお、インサートモードにおいて、関数の評価結果から入力文字列を挿入させる為には
指定をつける事で実現出来ます。
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.vimの
plugin/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
命令を使って関数を宣言する物を作ったとしましょう。
function! g: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で言うところの
with
や
Function.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-vim | Vimからhttp通信を行う際に便利なユーザエージェント、XMLパーサ、HTMLパーサ、JSONパーサ、OAuthライブラリ、Atomライブラリ、XMLRPCライブラリ、などWebのAPIに関する色々な物が入っています。私が開発しています。 |
wwwrenderer-vim | webapi-vimでHTMLをパースしたDOMからテキストブラウザの様な表現を行う為のライブラリです。私が開発しています。 |
vital.vim | バンドル型のライブラリセットで、ポータビリティ、バージョン管理に優れています。ujihisaさん、Shougoさん、tyruさん、thincaさん、と私が開発しています。 |
vimproc | Vimからプロセスを制御する事が出来るライブラリです。コンパイルが必要です。以前は中平さんが開発していた物をShougoさんが引き継いでいます。 |
vim-remote | Vimはプロセス間通信、スレッド間通信、非同期処理が弱いのですが、それをVimのremoteプロトコルを使って通信出来る様にライブラリ化した物です。中平さんが開発しています。 |
open-browser | 主にURLの抽出やブラウザの起動に特化したライブラリです。tyruさんが開発しています。 |
openbuf | バッファの生死管理やイベントハンドリングに使用します。thincaさんが開発しています。 |
その他、もっと沢山あって書ききれませんが、日本人が開発していて著名な所だと問い合わせも容易ですし、間違いなく日本語に対応しています。
上達方法
これについて先日、日本人Vimmer達が議論していましたが、ブログやヘルプ、
www.vim.org
にあるスクリプトを見て勉強したと言ってました。
まずはコードを読め...と言うのが先人からの教えでしょうか。
まとめ
Vim scriptの関数は他の言語から比べると、かなり取っ付きにくい性質を持っています。
DISられても当然と思われるでしょう。しかしながらこれはエディタが先に生まれ、設定ファイルを記述するべくして生まれ、それがアプリケーションを記述する為に進化していったという過程を考えると、必要最低限な実装と制限であったと言えるでしょう。
今後、Vimがある限りVim scriptは無くならないでしょう。しかしその制限された中でも上記の様なトリッキーな技によって皆を沸かせる事が出来るのも、Vimが持っている性質であるのではないでしょうか。