vim scriptで字句解析！


この記事は[Vim Advent Calendar 2012](http://atnd.org/events/33746) : ATND 45日目の記事になります。
44日目は[@yomi322](http://twitter.com/)の[vimshellからtweetしてみよう](http://yomi322.hateblo.jp/entry/2013/01/13/141003)でした。


- - - - - - - - - - - - - - - - - - - - - - - -

## 前置き

皆さんはvim scriptで字句解析したいと思うことはありませんか？
私はvim scriptを書いていると字句解析器がとても欲しくなります。
正規表現であれこれ小難しいパースするのはめんどいですし、ロジックミスもあり得ます。

そんなわけで、ついこの前[vital.vim](https://github.com/vim-jp/vital.vim)にText.Lexer
という字句解析モジュールを入れてもらいましたので紹介したいと思います。

[vital.vim](https://github.com/vim-jp/vital.vim)の使い方がわからない、もしくは、とりあえず使ってみたいという人は、
NeoBundleなどで[vital.vim](https://github.com/vim-jp/vital.vim)をインストールしてもらった後、

```vim:
NeoBundle 'vim-jp/vital.vim'
```

vim script上で以下のようなコードを記述してもらえれば、このText.Lexerを使うこと
が出来ます。

```vim:
let s:V = vital#of('vital')
let s:L = s:V.import('Text.Lexer')
```

[vital.vim](https://github.com/vim-jp/vital.vim)の使い方を知っている人は適当に読み替えて読み進めて下さい。


## Text.Lexerの使い方

Text.Lexerの使い方はとてもシンプルです。
以下の簡単な例を示します。

```vim:
let s:V = vital#of('vital')
let s:L = s:V.import('Text.Lexer')

let patterns = [
\ ['WHITE_SPACES','\s\+'],
\ ['WORD','[a-zA-Z]\+'],
\ ]
let tokens = s:L.lexer(patterns).exec('hoge foo')
echo tokens
" [
"   {'label': 'WORD', 'col': 0, 'matched_text': 'hoge'},
"   {'label': 'WHITE_SPACES', 'col': 4, 'matched_text': ' '},
"   {'label': 'WORD', 'col': 5, 'matched_text': 'foo'}
" ]
```

コード見てもらえればText.Lexerの使い方はわかると思いますが、この`patterns`が
どのようなトークン(字句)に分割するかを定義しています。
で、`s:L.lexer()`に渡した後、`exec()`で文字列を解析しています。
解析の結果である`tokens`は、以下のキーを持つ辞書のリストです。

* `label` : ラベル。`patterns`の各リストの１つ目の要素にした文字列を表す。
* `col` : 何文字目にマッチしたかを表す。
* `matched_text` : マッチした文字列を表す。

もし、`patterns`のどれにもマッチしない場合には`[Text.Lexer] can not match. col:34`のような例外を投げます。
また、`patterns`は上から順にパターンマッチを行いますので、以下のようなコードの場合には、すべてが`ASSIGN`にマッチしてしまいます。

```vim:
let s:V = vital#of('vital')
let s:L = s:V.import('Text.Lexer')

let patterns = [
\ ['ASSIGN','='],
\ ['EQUAL','=='],
\ ['COMMA',','],
\ ]
let tokens = s:L.lexer(patterns).exec('==,=,==,==,=,=')
call filter(tokens,'v:val.label !=# "COMMA"')
echo tokens
" [
"   {'col': 0, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 1, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 3, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 5, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 6, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 8, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 9, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 11, 'label': 'ASSIGN', 'matched_text': '='},
"   {'col': 13, 'label': 'ASSIGN', 'matched_text': '='}
" ]
```

まぁ、Text.Lexerはこれだけの仕事しかしません。あとは結果を焼くなり煮るなりしちゃって下さいw。


## Text.Lexerのサンプル

実際にText.Lexerはこんな風に使えるではないかという例をいくつか紹介したいと思います。


### サンプル.1

ホワイトスペースとコメントを除去したい。

```vim:
let patterns = [
\ ['IGNORE','\s\+'],
\ ['IGNORE', '//.*$'],
\ ['IGNORE','\n'],
\ ['WORD','\i\+'],
\ ]
let tokens = s:L.lexer(patterns).exec("hoge foo  \n bar // piyo")
call filter(tokens,'v:val.label !=# "IGNORE"')
echo tokens
" [
"  {'col': 0, 'label': 'WORD', 'matched_text': 'hoge'},
"  {'col': 5, 'label': 'WORD', 'matched_text': 'foo'},
"  {'col': 12, 'label': 'WORD', 'matched_text': 'bar'}
" ]
```


### サンプル.2

Vim文字列を抜き出す。(まぁ実際に使うにはコメント処理とか入れないといけないのでもうちょっとこらないと...)

```vim:
let patterns = [
\ ['NOT_STRING','[^"'']\+'],
\ ['STRING','"\(\\.\|[^"]\)*"'],
\ ['STRING','''\(''''\|[^'']\)*'''],
\ ]
let lexer_obj = L.lexer(patterns)
let tokens = lexer_obj.exec('call substitute(''v:val.label !=# "HOGE"'',''HOGE'',''FOO'',"g")')
call filter(tokens,'v:val.label ==# "STRING"')
echo map(tokens,'eval(v:val.matched_text)')
" ['v:val.label !=# "HOGE"', 'HOGE', 'FOO', 'g']
```

- - - - - - - - - - - - - - - - - - - - - - - -

以上、Text.Lexerは非常にシンプルだけど使える場面は何かと多いと思います。  
この機に[vital.vim](https://github.com/vim-jp/vital.vim)も挑戦してみてはいかがでしょうか？  

明日は[@Shaula__](http://twitter.com/Shaula__)さんです。