読者です 読者をやめる 読者になる 読者になる

Windows上でVimからプログラムに引数を渡したい時の注意点

Vim

Vim Advent Calendar 2013 の 63日目の記事です。


vim_useに「Escaping for system()」というスレッドがあったので、
Windows上でVimからプログラムに引数を渡したい時の注意点を簡単にまとめて返信したので、日本語でも書いてみます。

まず

VimからWindowsプログラムに引数を渡すにはいくつか難しい点があります。
特に複数の引数や特殊な文字*1を渡す場合等にハマります。

Vim側の問題

Windowsでは、'shellslash'オプションがオンの時、shellescape()は文字列をシングルクォートで囲って返します。
しかしcmd.exeはダブルクォートを使うので、シングルクォートで囲っても単純にシングルクォートがそのまま渡されてしまいます。
なので私はWindowsかつ'shellslash'オプションがオンの時でも期待通りに動くshellescape()関数を独自実装して使っています。
vital.vim/Process.vim at 9159770df1ba4b7b63da7a20fa7906bf66f6ebea · vim-jp/vital.vim · GitHub

Windows側の問題


Windows複数の引数をWin32APIレベルでサポートしていません。
どういう事かと言うと、新しいプロセスを開始するCreateProcess()関数は、プログラム名と引数を含めた単一のコマンドライン文字列を渡すよう設計されています。
例えばtest.txtというファイルをメモ帳で開きたい場合、CreateProcess()関数には"notepad.exe test.txt"という単一の文字列を渡します。
こういった設計なので、Windows上で実行されるプログラムは引数をパースしなければなりません。
しかしこのパース処理はスタートアップルーチンと呼ばれるものによって処理され、
スタートアップルーチンは大抵コンパイラによって自動生成される*2ため、
普通この処理を書く必要に駆られる事はありません。


しかしここで2つ問題があります。

  1. スタートアップルーチンを独自の解釈で実装しているプログラムがある
  2. cmd.exeとCreateProcess()のパース処理のソースコードがクローズドな事
スタートアップルーチンを独自の解釈で実装しているプログラムがある

例えば、cygwinに依存した*3実行ファイルはグロブ文字("*")といったUNIX系で使われる特殊な文字を独自で解釈します。

cmd.exeとCreateProcess()のパース処理のソースコードがクローズドな事

クローズドなので正確な処理を確認する事ができません。

もし思い通りの引数が渡ってこない場合、疑うべき3つの箇所

今回はVimのsystem()関数に関連するスレッドのため、
cmd.exe上からプログラムを起動する前提で話します。
この場合、3つ疑うべき箇所があります。
それは起動順で言うと以下の3つです。

  1. cmd.exe
  2. CreateProcess()
  3. スタートアップルーチン

流れとしては以下のような流れになります。

cmd.exe -> CreateProcess() -> スタートアップルーチン -> [プログラムが引数にアクセスする]

この3つの箇所のどこかで間違った解釈がなされているはずです。


ただ、分かったからといって対処できるかどうかは別ですが…
先程も言ったようにcmd.exeとCreateProcess()のパース処理のソースコードはクローズドなため、
疑う事しかできない場合もあります。
ただどの時点でまずい事になっているか分かればどのようなエスケープ処理をするか、
等の対策が立てられる…場合もあります(どうしようもない場合もある)。

結論

VimからWindowsプログラムに引数を渡すのは難しい。
プログラムによって引数の解釈が変わるため正しい道はなく、
「大体合ってる」位の方法しかWindows開発者には選択肢がない。

関連リンク

*1:スペース、^等、cmd.exeにとっての特殊な文字もあれば、cygwinの実行ファイルの場合*等の文字も独自の解釈がなされる場合もあります

*2:C言語ではスタートアップルーチンはmain()関数の前に呼び出されます。

*3:cygwin.dllに依存した?