« 読了:06/08まで (F) | メイン | 読了:06/26まで (C) »
2009年6月20日 (土)
今週ずっと悩んでいたことがあった。冷静に考えると,どうでもいいような話なのだが,まあいいや,解決した記念に記録しておこう。
なにか事情があって,コマンドラインで動くプログラムをVBAで呼ばないといけない,としよう。別にIT関係の仕事をしているわけでもないのに,なんでそんなことをしなければならないかは,ちょっと横に置いておく。人生にはいろいろ不条理な事が多いのだ。
いちばん手っ取り早いのはshell関数だ。
Sub Sample()
Shell "hogehoge.exe"
End Sub
という感じだ。
ところが,shell関数は外部プログラムを非同期に実行する。hogehoge.exeがなにをはじめようが,その終了を待たずにつぎの処理に移ってしまうわけだ。VB.NETではどうだかしらんが,少なくともVBAではそのはずである。同期実行したい場合はどうするか? かつてはAPI関数で終了を監視したものだが,最近ではWindows Scripting Hostを使うという手があるのだそうだ。
Sub Sample()
dim oWsh as object
set oWsh=createobject("WScript.Shell")
oWsh.run "hogehoge.exe", , True
set oWsh=nothing
End Sub
テストしてないけど,こんな感じで動くらしい。
そうすると欲が出てきて,こんどはhogehoge.exeになにか標準入力を与えたい,という気分になる。hogehoge.exeを実行すると,「爆発します。よろしいですか? (Y/N)」というプロンプトが出て,Nのキーを押さないとえらいことになる,としよう。RunメソッドのかわりにExecメソッドをつかえば,
Sub Sample()
dim oWsh as object
dim oExec as object
set oWsh=createobject("WScript.Shell")
set oExec=oWsh.Exec("hogehoge.exe")
oExec.StdIn.Writeline("Y")
set oExec=nothing
set oWsh=nothing
End Sub
このように,Stdin.WriteLineメソッドでhogehoge.exeにとっての標準入力を制御できる。ああ,これだと爆発してしまうけど。
さて,本題はここからだ。hogehoge.exeが,標準入力に一行与えると標準出力に数行吐く,また一行与えるとまた一行吐く...というようなプログラムであった場合を考える。なんでもいいけど,たとえばMeCabのようなプログラムである。形態素解析ソフトMeCabは,コマンドラインでこんなふうに対話的に動く。
C:\>mecab ←入力
さよならだけが ←標準入力
さよなら 感動詞,*,*,*,*,*,さよなら,サヨナラ,サヨナ
だけ 助詞,副助詞,*,*,*,*,だけ,ダケ,ダケ
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
EOS
人生だ ←標準入力
人生 名詞,一般,*,*,*,*,人生,ジンセイ,ジンセイ
だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
EOS
コマンドラインでmecab.exeを実行するとすぐに入力待ちになる。一行入れるとどどどと結果を返し(上の例では4行),また入力待ちになる。また一行入れるとどどどと結果を返し(上の例では3行)...CTRL-Zをいれるまで続く。このやりとりをVBAにやらせるにはどうすればよいか?
標準出力ストリームはExecオブジェクトのStdOutプロパティで参照できる。ストリームを一発で読み込むためにreadallというメソッドが用意されている。だから素直に考えれば,
Sub Sample()
dim oWsh as object
dim oExec as object
set oWsh=createobject("WScript.Shell")
set oExec=oWsh.Exec("mecab.exe")
oExec.StdIn.Writeline("さよならだけが")
debug.print oExec.StdOut.readall
oExec.StdIn.Writeline("人生だ")
debug.print oExec.StdOut.readall
set oExec=nothing
set oWsh=nothing
End Sub
ところが,これがうまくいかない。上の例だと,最初のdebug.printでプログラムが止まってしまい,なにも表示されていない黒いウィンドウと向き合う羽目になる。CTRL-Cをいれればウィンドウは消えるが,当然つぎのwritelineでエラーとなる。
あれこれ試してみたところ,どうやらプログラム実行中に標準出力をreadallすると,そこで止まってしまうらしい。しかし,実行中に標準出力が読めないというのはちょっと信じがたい。答えを求めてwebを徘徊し,悩みに悩み,ついにはうんざりして投げ出したが,どうにも気分が悪い。一晩経って気を取り直し,もう一度だけ試してあきらめることにした。
ふと思いついて,標準出力を1行ずつ読んでみると,
oExec.StdIn.Writeline("さよならだけが")
Dim i As Integer
For i = 1 To 4
Debug.Print i; oExec.stdout.readline
Next i
なんと,これは動く。ところが,oExec.StdOut.ReadLineをもう一行増やすと,その行で止まる。つまり,4行の標準出力が出ているとき,きっかり4行読む分には問題がないが,5行読もうとすると(ないしreadallで一気に読もうとすると),そこで止まるのである。おそらく,標準出力に5行目が吐かれるのを,じっと待っているのだろう。
不思議で仕方ないのは,AtEndOfStream プロパティを参照するだけでも止まる,という点である。MSDNによれば,AtEndOfStream プロパティには「ストリームの最後に達したかどうかを示すブール値が格納されます」ということだから,4行読むまではfalse, 読んだあとならtrueを返すはずである。だったらこれを門番にして
oExec.StdIn.Writeline("さよならだけが")
while not oExec.stdout.AtEndOfStream
Debug.Print oExec.stdout.readline
wend
と書けそうなものだ。実際,こういう例文があっちこっちに載っている。しかし,realineで4行読み尽くした後にAtEndOfStreamを参照すると,trueを返すのではなく,とにかく止まってしまうのである。4行目がほんとにストリームの最後だったのかどうか,様子を見守っているのだろう。どうやら,AtEndOfStreamが使い物になるのは,呼んだプログラムがもう終わってしまっている場合に限るようである。
MeCabの例でいえば,各回の最終行の中身がかならず文字列"EOS"になるので(そのように設定できるので),
oExec.StdIn.Writeline("さよならだけが")
dim sLine as string
sLine = vbNullstring
While sLine <> "EOS"
sLine = oExec.stdout.readline
Debug.Print sLine
Wend
というように,"EOS"が出てくるまでreadlineすればよい。今回やりたかったことに限っていえば,このやり方でなんとか解決できた。ほっとしたけど,ちょっと後味が悪い面もある。もし出力の中身から判別できなかったらどうすんのかね。。。
解決した記念に書いてはみたが,読み返してみると,実にどうでもいい話である。なんというか,出世するタイプの人が悩む問題ではないね。
雑記 - ものすごく生産的な話題