Quantcast
Channel: Vimタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 5608

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

$
0
0

この記事は 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
endfunctioncall Hoge()function!s:fuga()
    three
endfunctioncalls:fuga()function!s:piyo()calls:fuga()endfunctioncalls:piyo()lets:dict= {}
function!s:dict.aaa()
    four
endfunctioncalls: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!messagesredir END

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

    ifv:lang=~# 'ja_JP'let regex_error_detect ='^.\+\ze の処理中にエラーが検出されました:$'let regex_line ='^行\s\+\zs\d\+\ze:$'let regex_last_set ='最後にセットしたスクリプト: \zs\f\+'elselet 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\+'endiffor 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

                    letfilename= matchstr(verbose_func, regex_last_set)letfilename= expand(filename)if!has_key(files,filename)letfiles[filename] = readfile(filename)endifif 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\+'foriin range(1, max_line -2)let func_lines[i] ='^\s*' . matchstr(func_lines[i],'^\d\+\s*\zs.*')endforlet func_lines[max_line -1] ='^\s*endf[unction]'let lnum =0while1let lnum =match(files[filename], func_lines[0], lnum)if lnum <0throw'No dictionary function'endiflet find_dic_func =1foriin range(1, max_line -1)iffiles[filename][lnum +i] !~# func_lines[i]
                                    let lnum = lnum +ilet find_dic_func =0breakendifendforif find_dic_func
                                breakendifendwhilelet func_name = matchstr(files[filename][lnum], func_lines[0])let lnum +=1+ offset
                    elselet func_name  = substitute(func_name,'<SNR>\d\+_','s:','')let lnum =match(files[filename],'^\s*fu\%[nction]!\=\s\+' . func_name)+1+ offset
                    endifcall add(qf_info_list, {
                    \   'filename': filename,
                    \   'lnum': lnum,
                    \   'text': func_name,
                    \})endforelse                " <filename> の処理中にエラーが検出されました:letfilename= expand(matchstr(line, regex_error_detect))let qf_info.filename= expand(filename)endifelseif line =~# regex_line
            " 行    1:let lnum = matchstr(line, regex_line)if len(qf_info_list)>0let qf_info_list[0]['lnum'] += lnum
            elselet qf_info.lnum = lnum
            endifelseif line =~# '^E'            " E492: エディタのコマンドではありません: onelet [nr, text] = matchlist(line,'^E\(\d\+\): \(.\+\)')[1:2]
            if len(qf_info_list)>0if len(qf_info_list)==1let qf_info_list[0]['nr'] = nr
                    let qf_info_list[0]['text'] ='in ' . qf_info_list[0]['text'] . ' | ' . text
                elseleti=0for val in qf_info_list
                        let val['nr'] = nr
                        let val['text'] ='#' . i . ' in ' . val['text'] . (i==0 ? (' | ' . text) : '')leti+=1endforendiflet qflist += qf_info_list
            elselet qf_info.nr = nr
                let qf_info.text = text
                call add(qflist, qf_info)endiflet qf_info = {}
            let qf_info_list = []
        endifendforreturn qflist
endfunction

command!-nargs=0 QfMessages calls: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を初期化しています。

おわり

ぜひ使ってみて下さい。


Viewing all articles
Browse latest Browse all 5608

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>