cmd.exe のコマンドラインの解釈と Windows と Linux のプロセス作成の違い

PHPescapeshellarg の実装が Windows だと謎すぎたので調べていたら下記の記事にたどり着きました。

わりと長いこと Windows を使っているつもりなのですが知らなかったこともあり、とても興味深いとともに、もう cmd.exe には関わりたくないと思いました。

環境変数の展開

フェーズ1の環境変数%APPDATA% みたいなの)の展開は、フェーズ2のコマンドラインの解釈やフェーズ3の特殊文字の処理よりも先にあったんですね、知りませんでした。

例えば %APPDATA% のような文字をコマンドに渡したい場合、次のように書いていましたが・・・

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- ^%APPDATA^%
%APPDATA%[EOF]

これは %^(キャレット)でエスケープしているからではなかったんですね。

いや、エスケープしているといえばエスケープしているのですが、^%特殊文字としての効果を打ち消しているわけではなく、単に APPDATA^ などという環境変数は存在しないから展開されないのですね。

なので、2つあるキャレットのうち、先頭部分は意味がないです。次のように2番目のキャレットだけで環境変数の展開を抑止できます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- %APPDATA^%
%APPDATA%[EOF]

逆に1番目のキャレットだけだと環境変数が展開されます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- ^%APPDATA%
C:\Users\oreore\AppData\Roaming[EOF]

また、次のようにシェルの特殊文字環境変数に入れた場合、その特殊文字は有効です。

> set PIPE=^|
> echo hogehoge %PIPE% php -r "echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);"
PHP: hogehoge

bash だとそうはならないです。

$ export PIPE="|"
$ echo hogehoge $PIPE php -r "echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);"
hogehoge | php -r echo 'PHP: '; stream_copy_to_stream(STDIN, STDOUT);

ダブルクオートのエスケープ

ダブルクオートを文字として渡したい場合、ダブルクオートの中でダブルクオートを2つ繋げていました。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- "A""B""""C"
A"B""C[EOF]

が、バックスラッシュでもできたんですね。なぜか知りませんでした。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A\"B\"\"C
A"B""C[EOF]

がしかし、バックスラッシュの次の動作はキモいです。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- \\\\\\\\\"
\\\\"[EOF]
> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- \\\\\\\\\
\\\\\\\\\[EOF]

バックスラッシュは " に前置で連続しているときだけ \ 自身をエスケープし、それ以外の \ はエスケープしません。

Windows は UNC で \\ComputerName\SharedFolder のような記述をするので、バックスラッシュ単独ではエスケープされないようにしたかったからでしょうかね、予想ですけど。

WindowsLinux のプロセス作成のコマンド引数の違い

どちらかといえば最初は Windows 畑で育ったわたしは元々知っていましたが、参考記事のコメントにも書かれている通り Windows ではプロセスを作成するときのコマンド引数の指定の方法が Linux とは全く違います。

Linux だとコマンド引数は呼び出し側(通常はシェル)がコマンド引数の文字列を配列にバラします。

system(3) だとコマンドラインの文字列そのものを指定しますが、これはシェルを経由するからであって、コマンド引数の文字列はシェルが配列にバラしています。

PHP で言うと pcntl_exec だけはコマンド引数を配列で渡すのに対して、その他の普通のプロセス実行系の関数はコマンドライン全体を文字列として渡します(shell_exec など)。これは、pcntl_exec 以外はすべてシェルを経由するからです。

node.js なら child_process.execchild_process.spawn にその違いが現れています。

他の言語でもたぶん同じです。シェルを経由するのであればコマンドライン全体を単一の文字列で指定し、シェルを経由しないのであればコマンドのパスと引数の配列を指定します。もし、シェルを経由しないにも関わらずコマンド引数を配列ではなく文字列で指定できるのであれば、それはホスト言語が自前で配列にバラしているはずです。

一方、Windows だとプロセスを作成するときには、実行するバイナリのパス、と、コマンド引数、の2つの文字列を指定します(文字列と文字列の配列、では無く)。

なので、パイプやリダイレクト、および、実行するバイナリのパスとその後の文字列の分割、までは cmd.exe の仕事ですが、コマンド引数の文字列を配列 argv[] にバラすのは呼び出された側の仕事です。

C言語なら CRT の仕事です。MSVC なら真のエントリポイントである mainCRTStartup が配列にバラして main 関数の argv[] にセットします。おそらく CommandLineToArgvW 関数 が使われているのではないでしょうか。参考記事によると ruby は独自に解釈しているっぽいし、gcc でビルドしたバイナリも解釈が異なるようです。

ソースを確認したわけではないですが、rubymain 関数の argv[] は無視して GetCommandLine 関数 かなにかでコマンドラインを取得して独自に解析しているのではないでしょうか? まさか ruby のための CRT がある、なんてことは無いと思いますので。

Linux でもシェルによってコマンド引数の文字列を配列にバラすルールは微妙に違うかもしれませんが、POSIX の sh で最低限の仕様は担保されているのだと思います(system(3) なんかはユーザーのデフォルトシェルに関わらず /bin/sh が呼ばれるんでしたっけ?)。

が、Windows だと実行するコマンドによってその動作はバラバラなので、汎用的な escapeshellarg 関数など作りようがありません。

コマンド引数に改行を含める

参考記事には記述がありませんでしたが、cmd.exe ではキャレットを改行の直前に記述するとその改行が除去されます。

> php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^
More? Z
AZ[EOF]

Bash で言うところの改行の直前の \ と同じです。

More? はプロンプトの文字なので無視してください。バッチファイルの方が判りやすいですね。

a.bat

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^
Z
> a.bat
AZ[EOF]

また、キャレットの後の改行の直後が改行だと、改行になります。つまり "^\n\n" が改行になります。

a.bat

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^

Z
> a.bat
A
Z[EOF]

改行を複数入れたければ次のようにします。

@php -r "echo $argv[1] . '[EOF]' . PHP_EOL;" -- A^

^

^

Z
> a.bat
A


Z[EOF]

が、しかし、MSVC の system 関数だと改行以降の文字が無視されました。

a.c

#include <stdio.h>
#include <stdlib.h>

int main()
{
    system("php -r \"echo $argv[1] . '[EOF]' . PHP_EOL;\" -- A^\n\nZ");
    return 0;
}
> a.exe
A[EOF]

うーん、謎。

さいごに

とりあえずの結論としては、WindowsPHP では escapeshellarg は使わないほうが良いと思います。あるいはメタ文字っぽいものが無いことをあらかじめチェックした上で使った方が良いです。メタ文字を含む文字をコマンドに渡すために使う関数なのにメタ文字が渡せないなんて本末転倒もいいところですが。

あるいは proc_openbypass_shell オプション付きで使い、参考記事のフェーズ4のメタ文字だけどうにかすれば良いです。

プロセス実行系の関数に動的な文字を含めるようなことをしないのが一番良いのは間違いないですけどね。