Posted at
VimDay 6

Vim scriptのエラーメッセージをパースしてquickfixに表示する

More than 1 year has passed since last update.

この記事は Vim Advent Calendar 2016 の6日目の記事です。

Vim scriptのエラーが出たときにエラーをquickfixに表示して、エラー箇所に飛べるといいなと思いますが、標準の機能ではないようなので、コマンドを作りました。


先行事例

調べてみると、kanno_kannoさんが書いた記事

や、Stack Exchange

に参考となる方法が書いてありました。

kanno_kannoさんの記事では自らsourceしたときにでるエラーメッセージをquickfixに表示する方法が、Stack Exchangeにはtry catchで例外をキャッチしてquickfixに表示する方法が書いてありました。

それらを参考にして、すでにエラーメッセージとして出たエラーを:messagesで取得し、quickfixに表示するコマンドを作ります。


エラーメッセージの構造

まず、エラーを出すために、以下のようなファイルを用意します。


error.vim

one

function! Hoge()
two
endfunction
call Hoge()

function! s:fuga()
three
endfunction
call s:fuga()

function! s:piyo()
call s:fuga()
endfunction
call s:piyo()

let s:dict = {}
function! s:dict.aaa()
four
endfunction
call s:dict.aaa()


これを:source error.vimしてやると

/path/to/error.vim の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: one
function Hoge の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: two
function <SNR>253_fuga の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: three
function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: three
function 250 の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: four

のようなエラーメッセージが出ます。Vimのエラーは


  1. ○○ の処理中にエラーが検出されました:

  2. 行 <行番号>:

  3. <エラー番号>: <内容>

一つの単位であることがわかります。

それぞれのエラーを詳しく見ていきます。

/path/to/error.vim の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: one

1つ目のエラーは関数の中に入っていないときに例外が発生したときのエラーです。この場合は○○の部分はファイル名になり、行の部分は例外が発生したファイル内の行番号になります。

function Hoge の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: two

2つ目のエラーはグローバル関数の中で例外が発生したときのエラーです。この場合は○○の部分は関数名になり、行の部分は例外が発生した関数内の行番号になります。

function <SNR>253_fuga の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: three

3つ目のエラーはスクリプトローカルな関数の中で例外が発生したときのエラーです。関数名のs:が<SNR>数字_に変換されて表示されます。

function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: three

4つ目のエラーは関数内で呼び出した関数内で例外が発生したときにエラーです。[]の中に例外が発生した関数を呼び出した行番号が入り、..でつながります。行の部分は例外の発生源の関数内の行番号です。例外の発生源がさらに深い場合は..でさらにつながります。

function 250 の処理中にエラーが検出されました:

行 1:
E492: エディタのコマンドではありません: four

5つ目は辞書関数で例外が発生したときのエラーです。関数名は数字になります。


作ったコマンド

これらのエラーをパースして、quickfixに表示するためのVim scriptが以下になります。

function! s:qf_messages()

let str_messages = ''
redir => str_messages
silent! messages
redir END

let qflist = s:parse_error_messages(str_messages)
call setqflist(qflist, 'r')
cwindow
endfunction

function! s:parse_error_messages(messages) abort
" 戻り値。setqflistの引数に使う配列
let qflist = []
" qflistの要素になる辞書
let qf_info = {}
" qflistの要素となる辞書の配列。エラー内容がスタックトレースのときに使用
let qf_info_list = []
" 読み込んだファイルの内容をキャッシュしておくための辞書
let files = {}

if v:lang =~# 'ja_JP'
let regex_error_detect = '^.\+\ze の処理中にエラーが検出されました:$'
let regex_line = '^行\s\+\zs\d\+\ze:$'
let regex_last_set = '最後にセットしたスクリプト: \zs\f\+'
else
let regex_error_detect = '^Error detected while processing \zs.\+\ze:$'
let regex_line = '^line\s\+\zs\d\+\e:$'
let regex_last_set = 'Last set from \zs\f\+'
endif

for line in split(a:messages, "\n")
if line =~# regex_error_detect
" ... の処理中にエラーが検出されました:'
let matched = matchstr(line, regex_error_detect)
if matched =~# '^function'
" function <SNR>253_fuga の処理中にエラーが検出されました:
" function <SNR>253_piyo[1]..<SNR>253_fuga の処理中にエラーが検出されました:
let matched = matchstr(matched, '^function \zs\S*')
let stacks = reverse(split(matched, '\.\.'))
for stack in stacks
let [func_name, offset] = (stack =~# '\S\+\[\d')
\ ? matchlist(stack, '\(\S\+\)\[\(\d\+\)\]')[1:2]
\ : [stack, 0]

" 辞書関数の数字は{}で囲む
let func_name = func_name =~# '^\d\+$' ? '{' . func_name . '}' : func_name

redir => verbose_func
execute 'silent verbose function ' . func_name
redir END

let filename = matchstr(verbose_func, regex_last_set)
let filename = expand(filename)

if !has_key(files, filename)
let files[filename] = readfile(filename)
endif

if func_name =~# '{\d\+}'
let func_lines = split(verbose_func, "\n")
unlet func_lines[1]
let max_line = len(func_lines)
let func_lines[0] = '^\s*fu\%[nction]!\=\s\+\zs\S\+\.\S\+'

for i in range(1, max_line - 2)
let func_lines[i] = '^\s*' . matchstr(func_lines[i], '^\d\+\s*\zs.*')
endfor

let func_lines[max_line - 1] = '^\s*endf[unction]'

let lnum = 0
while 1
let lnum = match(files[filename], func_lines[0], lnum)

if lnum < 0
throw 'No dictionary function'
endif

let find_dic_func = 1
for i in range(1, max_line - 1)
if files[filename][lnum + i] !~# func_lines[i]
let lnum = lnum + i
let find_dic_func = 0
break
endif
endfor

if find_dic_func
break
endif
endwhile

let func_name = matchstr(files[filename][lnum], func_lines[0])
let lnum += 1 + offset
else
let func_name = substitute(func_name, '<SNR>\d\+_', 's:', '')
let lnum = match(files[filename], '^\s*fu\%[nction]!\=\s\+' . func_name) + 1 + offset
endif

call add(qf_info_list, {
\ 'filename': filename,
\ 'lnum': lnum,
\ 'text': func_name,
\})
endfor
else
" <filename> の処理中にエラーが検出されました:
let filename = expand(matchstr(line, regex_error_detect))
let qf_info.filename = expand(filename)
endif
elseif line =~# regex_line
" 行 1:
let lnum = matchstr(line, regex_line)
if len(qf_info_list) > 0
let qf_info_list[0]['lnum'] += lnum
else
let qf_info.lnum = lnum
endif
elseif line =~# '^E'
" E492: エディタのコマンドではありません: one
let [nr, text] = matchlist(line, '^E\(\d\+\): \(.\+\)')[1:2]
if len(qf_info_list) > 0
if len(qf_info_list) == 1
let qf_info_list[0]['nr'] = nr
let qf_info_list[0]['text'] = 'in ' . qf_info_list[0]['text'] . ' | ' . text
else
let i = 0
for val in qf_info_list
let val['nr'] = nr
let val['text'] = '#' . i . ' in ' . val['text'] . (i == 0 ? (' | ' . text) : '')
let i += 1
endfor
endif
let qflist += qf_info_list
else
let qf_info.nr = nr
let qf_info.text = text
call add(qflist, qf_info)
endif

let qf_info = {}
let qf_info_list = []
endif
endfor

return qflist
endfunction

command! -nargs=0 QfMessages call s:qf_messages()

このVim scriptをvimrcにかいて、エラーメッセージが出たら、:QfMessagesと打てば、quickfixにエラーが表示されるようになります。例えば、error.vimのエラーに対しては以下のようになります。

error.vim|1 error 492| エディタのコマンドではありません: one

error.vim|4 error 492| in Hoge | エディタのコマンドではありません: two
error.vim|9 error 492| in s:fuga | エディタのコマンドではありません: three
error.vim|9 error 492| #0 in s:fuga | エディタのコマンドではありません: three
error.vim|14 error 492| #1 in s:piyo
error.vim|21 error 492| in s:dict.aaa() | エディタのコマンドではありません: four

:messagesで表示される内容は:messages clearで消すことができます。


解説

Vim scriptの内容について見ていきます。


:QfMessages

コマンド:QfMessagess:qf_messages()を呼びます。


s:qf_messages()

s:qf_messages()messagesの内容を変数に入れて、s:parse_error_messages()を呼び出して、戻り値をquickfixにセットして、quickfixのwindowを開きます。


s:parse_error_messages()

s:parse_error_messagesはエラーメッセージをパースする関数です。

最初の部分で必要な変数を宣言しています。

日本語と英語のエラーメッセージにマッチする正規表現を用意しています。英語のエラーメッセージは

Error detected while processing /Users/tmsanrinsha/.cache/vim/junkfile/2016/10/2016-10-29-200145.vim:

line 1:
E492: Not an editor command: one
Error detected while processing function Hoge:
line 1:
E492: Not an editor command: two
Error detected while processing function <SNR>253_fuga:
line 1:
E492: Not an editor command: three
Error detected while processing function <SNR>253_piyo[1]..<SNR>253_fuga:
line 1:
E492: Not an editor command: three
Error detected while processing function 250:
line 1:
E492: Not an editor command: four

のようにな感じです。

for文でメッセージを行ごとに処理していきます。

エラー文は3種類あるので、それぞれif文の中で処理をしています。


○○ の処理中にエラーが検出されました

「○○ の処理中にエラーが検出されました」の行はさらに関数名かファイル名かで分岐します。


関数名の場合

関数のエラーの場合、関数の中で呼んでいる関数が例外を出すと、..でつながっていくので、..でsplit()します。原因の方が最初に来るようにreverse()しています。

分割した文字列ごとに関数名とエラーが発生した行番号(オフセット)を取り出します。発生源の関数のエラー発生箇所はエラーメッセージの次の行の「行 <行番号>:」に書かれため、ここでは0に仮置きしておきます。

辞書関数の数字は{}で囲みます。次のverbose functionを実行するときに必要だからです。

:verbose function Hogeなどとうつと

   function Hoge()

最後にセットしたスクリプト: /path/to/error.vim
1 two
endfunction

のような結果が表示されます。ここから関数が定義されたファイル名を取得することができます。

関数が定義された位置を取得するため、ファイルを読み込みます。

辞書関数に関しては数字しかわからないため、verboseに表示された関数の内容と一致する箇所をファイル内から愚直に探しています。

その他の関数については、関数名が書かれている行をファイルから見つけます。スクリプトローカルな関数については<SNR>数字_s:に変更してから探します。

見つけたらoffset部分を加えて、qf_info_listに情報を付け加えます。

これを関数分繰り返します。


ファイル名の場合

ファイル名を単にqf_info.filenameに入れます


行 <行番号>:

行番号を取り出します。qf_info_listに要素があるときは、関数内の行番号を示しているので、発生源の関数の情報を入れているqf_info_list[0]['lnum']に行番号を足します。


<エラー番号>: <内容>

ここではエラー番号とエラー内容を取り出します。関数の場合はin <関数名>をエラー内容を加えます。また、スタックがある場合は#<数字>を追加します。

また、この行はエラーの塊の最後の行なので、aflistに加え、qf_infoqf_info_listを初期化しています。


おわり

ぜひ使ってみて下さい。