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

Windows と Linux (*nix) のコマンドライン引数の違い

元は下記で回答したものですけど。。。


Linux でプログラムを実行するとき、最終的に次の関数が実行されます。

int execve(const char *filename, char *const argv[], char *const envp[]);

つまり、次のものが必要とされます。

  • 実行ファイル名
  • コマンド引数の配列
  • 環境変数の配列

一方、PHP のような言語から外部コマンドを実行したい場合、大抵は次のようなものを指定すると思います。

  • コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの)

とりあえず環境変数は無視しまして・・・前述の通り最終的に必要なのは「実行ファイル名」と「コマンド引数の配列」なので「コマンドライン文字列」をバラバラに分割する必要がありますが、それを行っているのがいわゆる「シェル」です。

なので「コマンドライン文字列」を指定してコマンドを実行する場合は、大抵の場合シェルを経由します。

例えば、Node.js の child_process モジュールを見てみると、シェルを経由する child_process.exec は「コマンドライン文字列」を指定するのにたいして、シェルを経由しない child_process.spawn では「実行ファイル名」と「コマンド引数の配列」を指定するようになっています。

PHP でもシェルを経由しない pcntl_exec は「実行ファイル名」と「コマンド引数の配列」を指定します。

つまり「コマンドライン文字列」を指定して外部プログラムを実行できるようにするためにはシェルを経由する必要があります。

もちろん「コマンドライン文字列」をプログラミング言語の方でバラバラに分割するようにすればシェルを経由せずに実行することも可能です。例えば Ruby では、一部のメタ文字を含んでいない場合は Ruby の側で「コマンドライン文字列」を「実行ファイル名とコマンド引数の配列」に分割することでシェルを経由せずに実行するようです。

メタ文字を含まない、ということはスペースで分割するだけで簡単にコマンドライン文字列をバラすことができるので、そのような実装になっているのだと思います(メタ文字を含む場合はシェルと同等のパーサが必要になりますしリダイレクトやパイプなどのシェル特有の事情も出てきてしまいます)。

なお、これらの事情は Linux などの *nix 系 OS の事情であって Windows では異なります。

Windows だと外部プログラムを実行するときの API で「実行ファイル名」と「コマンドライン引数(コマンド引数をスペースで繋げた文字列)」を指定します。

BOOL CreateProcess(
  LPCTSTR lpApplicationName,                 // 実行可能モジュールの名前
  LPTSTR lpCommandLine,                      // コマンドラインの文字列
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // セキュリティ記述子
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // セキュリティ記述子
  BOOL bInheritHandles,                      // ハンドルの継承オプション
  DWORD dwCreationFlags,                     // 作成のフラグ
  LPVOID lpEnvironment,                      // 新しい環境ブロック
  LPCTSTR lpCurrentDirectory,                // カレントディレクトリの名前
  LPSTARTUPINFO lpStartupInfo,               // スタートアップ情報
  LPPROCESS_INFORMATION lpProcessInformation // プロセス情報
);

それどころか lpApplicationName は省略可能なので、「コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの」だけでも可能です。

つまり Windows の場合はシェルを経由させなくても「コマンドライン文字列」を指定して外部プログラムを実行することができます。

そのためなのかどうかわかりませんが PHP の proc_open には Windows にだけ bypass_shell というシェルを経由しないオプションがあります。


そういえば @do_aki さんに教えてもらったんですけど、過去に *nix でも bypass_shell をサポートするコミットがあったらしいです、Revert されていますけど。

bypass_shell を指定したときの第1引数が Windows だと文字列(コマンドライン)なのに対して *nix だと配列(コマンドとコマンド引数の配列)となってしまうためなのかなーと想像しています。


ちなみに libuv を見た感じ、配列で渡されたコマンドライン引数を CreateProcess に文字列で渡すために自前で結合とエスケープ処理しているみたいですね。

もちろん *nix なら配列をそのまま渡すだけなのでそういう面倒なことはしていません。


ちなみに *nix だと execveargv がそのまま mainargv に渡ってくる感じだと思うんですけど、Windows だとそもそもプロセス作成時には引数を配列ではなく単一の文字列で指定するので、それをバラして argv に入れるのは CRT のスタートアップコードの仕事です。

CRT なんてコンパイラによって違うため(VC とか gcc とか)、コマンドライン文字列がどのようにバラされて argv になるかは微妙にブレがあります。

また、噂によると Ruby の Windows 版は CRT には頼らずに GetCommandLine かなにかで得たコマンドライン文字列を自前でバラしているとか。

そういう事情を鑑みると Windows で汎用的な escapeshellarg を作ることなんて不可能で とてもじゃないけど使い物になるようなシロモノではない のもしかたがないことだと思いますね。