Vim の +job を使ってみる

前回からの続きです。

secret-garden.hatenablog.com

前回書き忘れましたが今回使用する VimVim 7.4-2028 になります。
+channel +timer +job が追加された流れについて前回を参照してください。
ちなみにわたしも全部を把握しているわけではないのでところどころ曖昧です。

[+job とは]

+job とは外部コマンドを非同期で実行するための機能です。 今までは sysmte() で外部コマンドを実行していましたが、job_start() を利用する事が出来ます。 これにより vimproc.vim 等に依存することなく Vim の機能だけで非同期実行する事が出来ます。

[+job を使用する上での注意点]

前回も書きましたが、まだバグが残っている可能性もあるので、なるべく最新版の Vim を使用してください。 また、+job:help も日本語版ではなくて本体に付属してる英語版を使用してください(:help job@en

[外部コマンドを非同期で実行する]

では、早速使ってみましょう。 と、言っても +job を使うのはそこまで難しくなく、コールバック関数を指定すれば簡単に非同期で外部コマンドを実行する事が出来ます。

let s:count = 0

" job で実行された外部コマンドの結果を読みこむときに呼ばれる
function! Disp(ch, msg)
    let s:count += 1
    echom s:count a:msg
endfunction

" 第一引数に実行するコマンド(とオプション)を渡す
" 第二引数にオプションを辞書で渡す(今回はコールバック関数を指定)
" 戻り値に Job object が返ってくる
let s:job = job_start("vim --version",  { "callback" : "Disp"})

" 外部コマンドの実行をブロックせずにすぐに呼ばれる
echom "start"

[出力結果]

start
1 VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Jul 12 2016 13:33:51)
2 適用済パッチ: 1-2028
3 Compiled by worker@worker-System-Product-Name
4 Huge 版 with GTK2 GUI.  機能の一覧 有効(+)/無効(-)
5 +acl             +file_in_path    -mouse_sysmouse  -tag_any_white
6 +arabic          +find_in_path    +mouse_urxvt     -tcl
7 +autocmd         +float           +mouse_xterm     +termguicolors
8 +balloon_eval    +folding         +multi_byte      +terminfo
9 +browse          -footer          +multi_lang      +termresponse
10 ++builtin_terms  +fork()          -mzscheme        +textobjects
11 +byte_offset     +gettext         +netbeans_intg   +timers
12 +channel         -hangul_input    +num64           +title
13 +cindent         +iconv           +packages        +toolbar
14 +clientserver    +insert_expand   +path_extra      +user_commands
15 +clipboard       +job             +perl            +vertsplit
16 +cmdline_compl   +jumplist        +persistent_undo +virtualedit
17 +cmdline_hist    +keymap          +postscript      +visual
18 +cmdline_info    +langmap         +printer         +visualextra
19 +comments        +libcall         +profile         +viminfo
20 +conceal         +linebreak       +python/dyn      +vreplace
21 +cryptv          +lispindent      +python3/dyn     +wildignore
22 +cscope          +listcmds        +quickfix        +wildmenu
23 +cursorbind      +localmap        +reltime         +windows
24 +cursorshape     +lua             +rightleft       +writebackup
25 +dialog_con_gui  +menu            +ruby            +X11
26 +diff            +mksession       +scrollbind      -xfontset
27 +digraphs        +modify_fname    +signs           +xim
28 +dnd             +mouse           +smartindent     +xsmp_interact
29 -ebcdic          +mouseshape      +startuptime     +xterm_clipboard
30 +emacs_tags      +mouse_dec       +statusline      -xterm_save
31 +eval            +mouse_gpm       -sun_workshop    +xpm
32 +ex_extra        -mouse_jsbterm   +syntax          
33 +extra_search    +mouse_netterm   +tag_binary      
34 +farsi           +mouse_sgr       +tag_old_static  
35       システム vimrc: "$VIM/vimrc"
36       ユーザー vimrc: "$HOME/.vimrc"
372ユーザー vimrc: "~/.vim/vimrc"
38        ユーザー exrc: "$HOME/.exrc"
39      システム gvimrc: "$VIM/gvimrc"
40      ユーザー gvimrc: "$HOME/.gvimrc"
412ユーザー gvimrc: "~/.vim/gvimrc"
42     システムメニュー: "$VIMRUNTIME/menu.vim"
43        省略時の $VIM: "/usr/local/share/vim"
44 コンパイル: gcc -c -I. -Iproto -DHAVE_CONFIG_H -DFEAT_GUI_GTK  -pthread -I/usr/include/gtk-2.0 -I/usr/lib/x86_64-linux-gnu/gtk-2.0/include -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/pango-1.0 -I/usr/include/gio-unix-2.0/ -I/usr/include/freetype2 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/pixman-1 -I/usr/include/libpng12 -I/usr/include/harfbuzz     -g -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
45 リンク: gcc   -L. -Wl,-Bsymbolic-functions -Wl,-z,relro -L/build/ruby2.2-l9vij4/ruby2.2-2.2.3/debian/lib -fstack-protector -rdynamic -Wl,-export-dynamic -Wl,-E   -L/usr/local/lib -Wl,--as-needed -o vim   -lgtk-x11-2.0 -lgdk-x11-2.0 -latk-1.0 -lgio-2.0 -lpangoft2-1.0 -lpangocairo-1.0 -lgdk_pixbuf-2.0 -lcairo -lpango-1.0 -lfontconfig -lgobject-2.0 -lglib-2.0 -lfreetype   -lSM -lICE -lXpm -lXt -lX11 -lXdmcp -lSM -lICE  -lm -ltinfo -lnsl  -lselinux   -lacl -lattr -lgpm -ldl  -L/usr/lib/x86_64-linux-gnu -lluajit-5.1 -Wl,-E  -fstack-protector -L/usr/local/lib  -L/usr/lib/perl/5.18/CORE -lperl -ldl -lm -lpthread -lcrypt    -lruby-2.2 -lpthread -lgmp -ldl -lcrypt -lm

job_start() を使用して外部コマンドを実行します。 出力結果を見てわかるとおり外部コマンドの実行時にメインプロセスをブロックせずに即座に :echom "start" が呼ばれているのがわかると思います。 また job_start() の戻り値の Job object を利用して外部コマンドの実行後にプロセスの制御を行うことも出来ます。

[コマンドを中断する]

job_stop() を使用して実行したコマンドのプロセスを中断する事が出来ます。

let s:job = job_start("vim --version")

" 現在の Job のステータスを表示
echom job_status(s:job)
" => run

" Job を強制終了させる
call job_stop(s:job, "kill")

echom job_status(s:job)
" => dead

[標準出力、標準エラーを分ける]

"callback" の変わりにそれぞれ "out_cb""err_cb" に対してコールバック関数を指定することで、標準出力と標準エラーを分けることが出来ます(正確にいえば、"out_cb" 等が設定されていない場合に "callback" が呼ばれます。

function! Stdout(ch, msg)
    echom "Stdout" a:msg
endfunction


function! Stderr(ch, msg)
    echom "Stderr" a:msg
endfunction


let s:opt = { "out_cb" : "Stdout", "err_cb" : "Stderr" }

" OK
" call job_start("vim --version",  s:opt)

" Error
call job_start("vimaaaaa --version",  s:opt)

" output:
" Stderr executing job failed: そのようなファイルやディレクトリはありません

[コマンドに " 含まれている時に上手く動作しない場合の対処方法]

これはわたしもよく原因を把握してるわけではないんですが、例えば次のようにコマンドに " に含まれている場合に上手く動作しない事があります。

function! Disp(ch, msg)
    echom a:msg
endfunction

" 意図しては Ruby で "puts 42" というコードを実行させたいが上手く動作しない…
call job_start('ruby -e "puts 42"',  { "callback" : "Disp"})

こういう場合、コマンドを分割してリストで渡すと上手く動作します。

function! Disp(ch, msg)
    echom a:msg
endfunction

" コマンドをオプション単位で分割する
call job_start(['ruby', '-e', 'puts 42'],  { "callback" : "Disp"})

これで意図した動作します。

[バッファに出力する]

+job は実行結果を任意のバッファに出力することも出来ます。

call job_start("vim --version",  { "out_io" : "buffer", "out_name" : "job_test" })

" バッファを開く場合
" split job_test

"out_io""buffer" を指定し、"out_name" に出力されるバッファ名を指定します。 ちなみに "out_name" の変わりに "out_buf" を使用すればバッファ番号を指定することも出来ます。
これにより標準出力が指定したバッファ名の末尾に追加されます。 また、"out_io""out_name" では『標準出力』のみ出力されるので『標準エラー』を出力したい場合は "err_io""err_name" を別途指定する必要があります。

call job_start("vim --version",  {
\   "out_io" : "buffer",
\   "out_name" : "job_test",
\   "err_io" : "buffer",
\   "err_name" : "job_test"
\})

また、バッファ以外にもファイルなんかにも出力する事が出来ます。

[まとめ]

と、いう感じで簡単に使い方を紹介してみました。
正直、使う分には難しくないんですが『最低限の機能しか用意されていないので凝ったことをする場合は工夫が必要』というような感じですかね。
しかしながら今までこういう非同期処理を行う場合は vimproc.vim に依存する必要があったのでやはり Vim 本体に非同期機能が実装されたのはとても大きいことだと思います。
今はまだ安定してなかったり実装されていなかったばかりでそんなにガッツリと触ってる人は少ないと思いますが、これから Vim 本体がメジャーリリース等されればどんどん活用されていくような気がします。
今後、非同期処理を使用してどんなプラグインが出てくるのかが楽しみです。