Humanity

Edit the world by your favorite way

AppVeyor で Go のバージョンを固定する方法

基本的に Go アプリのテストは以下を参考にしたら概ねうまく言ってたんだけど、 (最新じゃなく)バージョン固定してる場合に問題となったため書いておく。

blog.markvincze.com

起こった問題

最近 AppVeyor の Go のバージョンが上がって Go 1.10 をデフォルトで使うようになった。 上記記事の設定だと GOROOT の定義が抜けてるために以下のようなエラーが出るようになった。

compile: version "go1.10" does not match go tool version "go1.9.4"

対応策

GOROOT 設定すればいいだけ。 今開発してる Volt というツールでは Go 1.9 でテストしたかったので以下のように指定した。 他のバージョンまたはアーキテクチャでテストしたい場合は C:\go19 じゃなく別のパスにする必要がある

volt/.appveyor.yml at feabe7feeecf2bddda135cc7e43cc188ed781d81 · vim-volt/volt · GitHub

差分は見る必要ないと思うけど一応こんな対応をした。

[AppVeyor] Fix go version mismatch by tyru · Pull Request #204 · vim-volt/volt · GitHub

解説

Vim プラグインのパッケージマネージャなので Vim のインストールとか色々書いてあるけど、 以下の記述だけ参考にすればいい。

  • GOPATH の設定
  • GOROOT の設定
  • PATH の設定
    • set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%

後は go build やら go test するだけ。 go build はビルドが通る事を確認するためのもの。 go test はテストするためのもの。 詳しくは上の設定見てください。

どうでもいいけどビルドが通ることを確認するなら Build configuration | AppVeyor を使った方がいい気がする。 まぁそれは後で。

参考リンク

ロガー用 vital モジュールを作った (ただし Vim 8 専用)

まだ vim-jp/vital.vim には PR してないけど十分使える (使えてる)。

https://github.com/tyru/nesk.vim/blob/0775e7d6fb3502ce52b64911d84b19cde32825aa/autoload/vital/__nesk__/Nesk/Log.vim https://github.com/tyru/nesk.vim/tree/0775e7d6fb3502ce52b64911d84b19cde32825aa/autoload/vital/__nesk__/Nesk/Log

ちなみに使ってるのはここら辺。

https://github.com/tyru/nesk.vim/blob/9d0422c3ac064183d64a137116ab305786c99248/autoload/vital/__nesk__/Nesk.vim#L21-L36 https://github.com/tyru/nesk.vim/blob/9d0422c3ac064183d64a137116ab305786c99248/autoload/vital/__nesk__/Nesk.vim#L395-L420

注意点

  • Vim 7.4 には対応してない (Vim 8 専用)
  • Log.XXX を実装する場合
    • logger.flush() を読んだ時点で出力する
    • logger.log(level, msg) を読んだ時点では出力しない
    • Log.File で1行ずつ writefile() したらめっちゃ遅かったので flush 前提の API とした
  • 遅い場合は {autoflush: 0} を指定する
    • デフォルトでは毎行 flush する
  • それでも遅い、特に Log.File が遅い場合は {file_redir: 1, autoflush: 0} を指定する
    • writefile() での追記が遅い ので :redir を使える場合は指定すると速くなる
    • デフォルトじゃないのは :redir はネストして使用した場合にエラーとなるため、 プラグイン:redir を使用している可能性があるのでデフォルトは writefile() の追記で実装している

出力先

  • Log.File: ファイル出力ロガー
  • Log.Echomsg: :echomsg 出力ロガー
  • Log.Nop: 何も出力しないロガー

Log.Noplogger.log(msg) を呼ぶのに if で分岐せずに済むようにするため。 レベルによらず全く出力させたくない場合はこの出力先にする。

上記のモジュールのオブジェクトは new() で生成できる。

  • Log.new({'output': 'File'})
    • file_path (String)
      • 例: '/path/to/logfile'
    • file_format (Funcref)
      • {options -> {level,msg -> ...}} のような Funcref を指定する。
      • 例 (デフォルト値): {options -> {level,msg -> printf('[%s] %s', get(options.levels[0], level, '?'), msg)}}
    • file_redir (Number | Bool)
      • もし true 値が指定されたら :redir を使ってファイル末尾に追記する。
      • デフォルトは writefile() で追記する。
  • Log.new({'output': 'Echomsg'})
    • echomsg_hl
      • :echohl の引数をリストで渡す。それぞれの要素の位置は options.levels と対応している。
      • デフォルト値: []
      • 例: ['None', 'WarningMsg', 'ErrorMsg']
    • echomsg_format
      • File の file_format と同じ
      • デフォルト値は file_format と違ってタイムスタンプも出力される
        • {options -> {level,msg -> printf('[%s] %s %s', get(options.levels[0], level, '?'), strftime('%Y-%m-%d %H:%M'), msg)}}
    • echomsg_nomsg
      • もし true 値が指定されたら :echo を使って出力する。
      • デフォルトは :echomsg で出力する。
  • Log.new({'output': 'Nop'})

Log.File.new(path)Log.Echomsg.new() としないのは切り替えをより楽にするため。 例えばデバッグのために Log.EchomsgLog.File に切り替えたいとする。 そうすると Log.File.new(path) の場合

  1. s:V.import() の引数 (モジュール名)
  2. Log.new() の引数

の両方を変えなければいけない。

Logger API

  • logger.set_level(level Number)

  • logger.log(level Number, msg String | Funcref)

  • logger.flush()
    • ImplLogger.flush() を呼び出す。

デフォルトではそれぞれのレベルに対応した以下のメソッドとラベルが追加される。 レベルに対応したメソッドは Logger.new(options Dictionary) の引数 options.levels で指定できる。

  • logger.info(msg String | Funcref)
  • logger.warn(msg String | Funcref)
  • logger.error(msg String | Funcref)

  • logger.INFO (0)

  • logger.WARN (1)
  • logger.ERROR (2)

ラベルは既存メソッド名(Logger.log)と被らなければ何でもいい。 [['INFO', 'info'], ['WARN', 'warn'], ['ERROR', 'error']] のような配列を渡せば メソッドとそれに対応するラベル定数を生成してくれる (ちなみにこの値は levels のデフォルト値)。 この場合それぞれのレベルは INFO < WARN < ERROR となる。 Dictionary ではなく List で指定するのはこのため。

出力先モジュールの API

出力先のモジュールは Log.new() によって裏で import() され new() される。 切替可能にするため以下のメソッドを実装しなければならない。以下がその API

  • implLogger.log(level Number, msg String)
  • implLogger.flush()

TODO

  • Vim 7.4 対応
  • vim-jp/vital.vim に PR する

最近の Vim で入った面白 Vim script 関数

:h eval.txt を眺めてたら結構色々関数追加されてるなーと思ったのでまとめてみた。 (追記: 結構古くからある関数も交じってた…) 基本的に詳細な仕様は書くつもりはありません (古くなるかもしれないし help 見た方が正確)。

  • wordcount()
    • g<C-g> を押すと現在のバッファのバイト数、文字数、単語数 (ビジュアルモードの時は選択されたバイト数、文字数、単語数も) を表示してくれますが、それを関数で取れるようにしたやつです
  • getcharsearch()
    • [tfTF] コマンド (例: f<char>) の情報を返してくれるやつです
    • どの char を押したか、前方検索 or 後方検索、[tT] か [fF] かを返してくれます
      • 要するに直前に [tfTF] コマンドのどれを使ったか、引数の char は何なのかの情報
  • eventhandler()
  • complete_check()
    • ニッチだけど補完系プラグインでは便利そう
    • 候補の取得に時間がかかった場合ユーザ入力があった場合に中断できる
  • getcompletion({pat}, {type} [, {filtered}])
    • set wildmode してればコマンドライン<Tab> を押せば補完が効くが、その補完の候補を取得できる
    • 例えば getcompletion('', 'shellcmd') で PATH が通った全ての実行ファイル名を取得できる (ちょっと時間かかる)
  • searchdecl({name} [, {global} [, {thisblock}]])
    • gdgD の関数版。ううーんできればカーソルの状態変えずに位置だけ返してほしかった…
  • getbufinfo([{expr}]) or getbufinfo([{dict}])
  • getwininfo([{winid}])
    • こんなんあったのか…!
    • :ls の結果を Dictionary で取れる…もう :ls をパースとかする必要なんてなかったんや…
    • (むしろなぜ今までなかったのか)
  • setbufline({expr}, {lnum}, {text})
    • なぜ今まで(ry)。前から Vimmer がほしいほしい言ってたやつ…
    • 別バッファの指定された行を追加/更新する関数
    • :terminal を実装する過程でひょっこり追加された記憶
  • Window ID 系
    • Window ID に関しては Vim 8.0 Advent Calendar 7 日目 ウィンドウ ID - Qiita
      • タブを跨いでも一意なのが便利
    • win_findbuf({bufnr})
      • bufnr のバッファを表示している Window ID のリストを返す
    • win_gotoid({expr})
      • 引数の window ID のウィンドウに移動する
      • execute tabnr 'tabn' | execute winnr 'wincmd w' とかやってたのが関数一つでできるようになった
    • win_id2tabwin({expr})
      • 引数の Window ID のタブ番号、ウィンドウ番号を返す
        • let [tabnr, winnr] = win_id2tabwin(winid)
  • index の代わりに N 文字目の N (以下文字インデックス) を受け取る文字列操作 (7.4 からあるものもあるけど)
    • (文字の定義はエンコーディング (encoding オプション) によって変わります)
    • strchars({expr} [, {skipcc}])
      • 文字数を返す
    • strcharpart({src}, {start} [, {len}])
      • 文字列のスライス (strpart()) の文字インデックス版
    • strgetchar({str}, {index})
      • 文字列の index (文字インデックス) の位置の文字の数値を返す
      • 文字列で取得したい場合は nr2char() で要変換
    • strwidth({expr})
      • 文字列の文字幅を返す
      • タブ文字の文字幅は1
    • strdisplaywidth({expr} [, {col}])
      • 基本 strwidth({expr}) と同じだが、タブ文字は tabstop, display オプションに影響される
  • タグファイル系
    • tagfiles()
    • taglist({expr} [, {filename}])
  • 実行ファイルパス取得
    • exepath('prog') で PATH 中の prog のフルパスを取得
  • changenr()
    • undotree().seq_last と同じ?
    • これいつから追加されたんだろう。もしかしたら結構古いかも
      • :helpgrep changenr したら version7.txt に載ってたので 7.0 で追加されたっぽいです

上のはちょっと便利系関数ですがこれはマジ便利なので知ってほしい系関数(語彙力)です。

  • Channel 系
    • ch_*()
  • Job 系
    • job_*()
  • Terminal 系
    • term_*()
  • 部分適用
    • function() で引数、self を束縛できるようになった
  • 小ネタ: Vim 8 からは関数名から Funcref を取得する時は function() よりも 新規追加された funcref() のが安全
    • function() は関数名の string で lookup しようとするので 同じ名前で再定義された場合は再定義後の function を見に行ってしまう
    • まぁ再定義とかあんまりないけど… (以下 2018/2/14 追記)
      • :functionclosure 属性を使う場合は頻繁に再定義されるケースがほとんどだと思います
      • 例えば以下のコードで外側の関数 (s:add()) が実行される度に内側の s:add1() は再定義されます
function! s:add(n)
  function! s:add1(m) closure
    return a:n + a:m
  endfunction
  return funcref('s:add1')
endfunction

例えば上記のコードで function() を使ってしまっていると、 以下のように期待と異なる結果が出力されてしまいます。

function! s:add(n)
  function! s:add1(m) closure
    return a:n + a:m
  endfunction

  " ここは funcref() を使うべき!
  return function('s:add1')
endfunction

let s:f = s:add(1)
let s:g = s:add(2)

" この時点で s:add1() が 2 + a:m に再定義されてしまっている。
" s:f は function() を使っているので "s:add1" という関数名で定義を探し出す。
" よって再定義後の関数を実行してしまう

" 1+1 = 2 のつもりが 3 が出力されてしまう
echo s:f(1)

funcref() ならば Funcref のリファレンスで関数の定義を探すため、このような事は起こりません。

おまけ: カーソル下の1文字を取得

  • getline()col('.')-1 (index) だとマルチバイト文字の場合にちぎれる
  • 以前までは normal! yl を使っていた
    • レジスタを退避したりしなければならなかったりして面倒

と思ってたけどこれでいいじゃんと思った。

echo matchstr(getline('.')[col('.')-1 :], '^.')

ちなみに Vim にはカーソルのバッファ中のバイトオフセットから行番号を取得する byte2line({byte}) という関数があるのだけど、なぜか byte2col({col}) という関数はない。 あれば正規表現使わず

echo nr2char(strgetchar(getline('.'), byte2col(wordcount().cursor_bytes)-1)

とかできるかなーとか思ったけど結局必要なかったので提案しようにも理由がなくなってしまったので決め手に欠ける…

set virtualedit=all とかして行末以降にカーソルがある状態を考慮してなかったのと、 col (=バッファのカーソルの位置) と行の文字列を比較するのも何か意味合いが違うなと感じたのでやっぱ無しで…