妥協案的にはまぁまぁ完: 「bogus」をエミュレートしたければ皿まで

これから始まった一連の話題の、「ひとまずの収束」。

最新リビジョン

ダメダこりゃ」が抜本的に措置できているわけではないんだけれど、「使い方次第」でどうとでもなる範疇、というよりか「そういう使い方が出来るように」拡張した。

問題にしているのは cmd.exe が「exe (などの実行ファイル)」と「batch ファイル呼び出し」を区別して振る舞いを変えてしまうことにあるんだど、「最小公倍数的なインターフェイス」にすることで逃げてみた。

話を最初から整理する。

まず、「何やってるの? そもそも」については、以下例でわかってもらえると信じている:

「split」がアタシの本題処理
 1 >>> import re
 2 >>> import json
 3 >>> _dump = lambda s, **kw: print(re.sub(
 4 ...     r"\s+$", "", json.dumps(split(s, **kw)), flags=re.M))
 5 >>>
 6 >>> # contiguous line
 7 >>> _dump("""\
 8 ... @ECHO OFF
 9 ... copy aaa bbb^
10 ... ccc""")
11 [["@ECHO", "OFF"], ["copy", "aaa", "bbbccc"]]

バッチファイルや、「cmd.exe に垂れ流す目的の文字列」を、それこそ subprocess モジュールでの(shell=True に依存しない)プロセスコールに渡す argv にしたいわけである。という限定的な目的だけでなく、「cmd.exe エミュレータの鍋の具」になったり、あるいは「バッチファイル変換君」の元ネタになったり、もしくは「特定のコマンドを別のものに差し替えたい(例えば clwrap とか)といったニーズにも使えるだろう。後者の「その他」目的のためにも、「argv に分解してから」行えることは重要である。そうしないと「単純な文字列置換」が意図しないところまで置換祭り、の呪縛から一生逃れられない。(典型的には「パス区切り文字」をスラッシュからバックスラッシュに変換したくて、「スイッチ」のスラッシュまで置き換えてしまう、のような。)

次に、「問題」の整理。

2つの検証用プログラム:

myechoexe.c
 1 /*
 2  * cl myechoexe.c
 3  */
 4 #include <stdio.h>
 5 int main(int ac, char **av)
 6 {
 7     int i = 1;
 8     for (; i < ac; ++i) {
 9         printf("%s\n", av[i]);
10     }
11     return 0;
12 }
myecho.bat
1 @echo %1
2 @echo %2
3 @echo %3

これを「(いわゆる) DOS プロンプト」から色々引数を変えて呼び出してみるわけである。これは MSYS などの Unix エミュレーションを介してはダメ。まさに素の cmd.exe から。

「どっちのパターンも不可解で不愉快」であることには違いはないものの、パースの基礎的な部分はかなり謎が解けた。つまり「いつ誰がどのようにどんな順序で引用符やカレット、区切り文字の評価をするのか」については、ワタシの実装はおそらく「かなり正解に肉薄している」と思う。無論まだ本格的にテストは必要だが、「不可解でおバカな振る舞い」をかなりちゃん模擬できている。実際「cmd.exe ユーザとして」叩いてると、びっくりというか笑える振る舞いをたくさんしてくれて、都度都度「なんでそーなる」と頭を掻き毟り、一歩ずつ一歩ずつ「実現方式を想像」していった。

「DOS のソースみりゃいいじゃん」て? いや、それは「不可能に限りなく近い」。現行 Windows の cmd.exe はまずオープンではなく、オープンになっているのは20年以上前の「本物の古い DOS」だけ。これはアセンブラで書かれてる。当時の DOS にカレットなどの仕様があったのかなどそもそも差異がわからんし、アタシは全然アセンブラ読めないしさ。当然「正確な振る舞いがドキュメントされてればそれ見ればよくね?」も無論ダメ。そんなもんはどこにもない。だから「振る舞いから実装を想像する」以外のアプローチは、ない。(cmd.exe をディスアセンブル…して解読出来るヒトではないもん、あたしゃ。)

さて、そうやって基礎的な部分の理解が出来、そして上の EXE パターンと batch パターンを注意深く差異を見ると、わかっている範囲ではこれら:

  1. batch の方だけ「;」「,」(などセパレータ)で分解されて渡される
  2. exe の方だけ二重引用符が剥がされて渡される

なのね。(まだあるはずだが今のところ、ね。)

だからワタシの「split」処理は、2. のために「二重引用符剥がしモードと剥がさないモード」を持つことにした。そして 1. の方は、そもそもどうやって「batch を呼び出したいのだ」という意図を汲み取るのか、という問題はあるにせよ、仮にそれがわかるのであれば、単に「arg」をさらに分解すれば良いだけだ。この手の扱いはここらでやってる。

というわけで、とりあえず「今ワタシが欲しい程度」のものとしてはだいたいいい感じ。具体的には MSBuild プロジェクトファイルの CustomBuild や PreBuildEvent、PostBuildEvent、PostLinkEvent で書かれるものに耐える用途。ただし MSBuild にはこちら固有のマクロ参照があるので、そっちは別のアプローチでひっぺがすんだけどね。

で、今は、書いたこれを、こういうようなこと への措置に使いたいと思っている。

完成度がアレなんでいつになることやら、だけれど、今はスニペットにペロっと貼り付けてるだけだけれど、少なくとも「パブリックなプロジェクトとして公開レポジトリを作って、置く」ことはする。PyPI に登録するとこまで行くかどうかはまだわかんない。もっと大きなプロジェクトの一味としたい気分もないでもないし、独立した方が扱いやすいという気分もあるし。なんであれ「広く皆が使えるもの」にはしたいと思う。(実際大変なのはドキュメント。結構気が滅入る。)


2017-11-20 追記:
「オレ的目的にはおけ」に概ね(?)二言はないけれど、「「広く皆が使えるもの」にはしたい」はキビシイ気がしている。そもそもここに書かれている説明と全然違う。少なくともこの違いをちゃんと説明出来ないなら、「ええもんだ」としての公開は出来るわけがない。「どういうときにどういう相手に使えるのか」こそが大事なわけじゃないか。

あと、「カレットを勝手に取っ払う」のも split 時点ではダメだろうなと思った。やっていいのは継続行のヤツだけ。残りは基本的に「argv に分解されてから」解釈されなければならない、て風にしないと、「戻せない」。

てわけで「説明出来る/出来ない」とは関係なく、「アタシが今目にしてる cmd.exe を模擬する」ということに限ってもまだやること多いのでどっちみち「ちゃんとした公開」はすぐには出来ない、てことにはなる。

まぁ「オレ的にはおけ」がかえって問題でさぁ、、、なんせ今の状態でも今の自分のニーズにはほとんど大満足だったりもするんだよね。


2017-11-23 追記:
Wine に cmd.exe 互換実装が含まれていることを知った(GitHub へのミラー)。Wine は基本「Unix 系 OS で「Windows でそのまま動作する exe などを「そのまま動かす」」というエミュレーションなので、独自に互換実装を持っているとは思ってなかった。あ、でもそりゃそうか、Windows ネイティブの実行ファイルが cmd.exe に依存してる、なんて、あるだろうし、そもそもこれなかったら「恐怖のバッチファイル」を動かせないことになるものね。

で、WCMD_IsEndQuoteWCMD_parse を斜め読みしているのだが、これが本当に「互換実装」なのだとしたら、どうにも現行 Windows 7 に搭載されてる cmd.exe とは振る舞いが異なるんじゃないかという気がする。WCMD_IsEndQuote から察するに、「"a " b " c "」を、「一番外の引用符がつえーんだぜっ」として内側を包もうとしてるようにしかみえない。つまり「"a " b " c "」は一つのパラメータであってセパレータで分割されない。そしてこの振る舞いこそが、ここに書かれている説明だ。

もしもワタシのソースコード読解が正しいとして、なおかつ「本当に完全互換実装を狙った実装」なのだとすれば、考えられることはただ一つ。「cmd.exe の振る舞いが変わった」。うーん、そうなの? いかに「ダメな振る舞いだった」としても、こういう根本的な振る舞いの変更は、おいそれとはやっちゃいかんヤツであり、Microsoft ってそういうことあんましないんだよなぁ…。わからんよ、全然わからん。

wcmdmain.c の該当部分を引っこ抜いて Windows でビルドするのは造作ないような気もするし、wine なら VirtualBox にインストールしてる linux で動かせるから、あとで実際に動かしてみようかね。斜め読みしかしてないんで、読み間違えてる可能性も高いもの。

別に「今の cmd.exe だけ考える」んでも、状況によってはいいんだけどね、例えば「Windows 2000 までは違ってた」なら安心して黙殺していい。が、「Windows XP」だとちょっとだけ話が違ってくる。XP は「まだ死んでまもない」わけで、「XP をターゲットに書かれたプロジェクト」というのはまだまだ全然死んでない。というか「ちょっとだけ古いプロジェクト」のソースコードが XP を前提に書かれている、なんてのは全然少なくはない。そうならかなりイヤらしい。まぁ、XP ユーザ、はさすがに多くはないとは思うけれど。

どうでもいいが Wine は「Unix で」動作するものだ。だから Wine の cmd.exe 互換は「普通の Unix シェル」から呼ばれることになる、コマンドラインから使うつもりなら。頭痛いよ。「完全互換」として振舞うには、コマンドラインを解析させてはダメで、ファイルを入力にする必要がある。「Unix シェルから呼ばれる」前提で書かれてるとちょっと困ってしまうが、まぁそんな器用なことはしないだろう、普通。(無論 Windows で動作させる場合も「本物の cmd.exe から引数を指定して起動」してはダメ、これもファイルをパースさせないといけない。)


2017-11-23 さらに追記:
ここ:

note that this behavior depends on the application. In Windows, each application parses its own command line parameters. I believe the behavior you’re describing is that of Microsoft’s standard C library,

なんて説明をしている人がいるのだが、腑に落ちないし、これが本当ならかなりぎょっとする。つまり C で書いたとして、「cmd.exe がプログラムに未分割の argv を渡す ⇒ 自分が書く main() の前に C ランタイムが argv 分解を行う」ということを言っているらしい、となるからだ。これが Win16 時代は真だったのは知っている。けれどこれは今は違うのではないのか?どうやったら確認出来るかいな、と…:

a.c
1 #include <stdio.h>
2 int main(int ac, char **av)
3 {
4     int i = 1;
5     for (; i < ac; ++i) {
6         printf("[%s]\n", av[i]);
7     }
8     return 0;
9 }
ビルド
1 d:\do\s\mes> cl a.c
cmd.exe から実行
1 d:\do\s\mes> a "3\" of snow" "& ver."
2 [3" of snow]
3 []
4 
5 Microsoft Windows [Version 6.1.7601]
python から subprocess で shell=False で実行
1 >>> import subprocess
2 >>> rv = subprocess.check_call(
3 ...     [
4 ...         "a.exe",
5 ...         r'''"3\" of snow" "& ver."'''
6 ...     ])
7 ["3\" of snow" "& ver."]

ちぅわけでガセだ。セパレータで分解してるのは「Visual C++ CRT」ではなく cmd.exe である。迷惑な情報を撒き散らさないでくれ、と本音では言いたいけれど、正直一方では「気持ちはわかる」。それだけ「cmd.exe (やその前身の COMMAND.COM)」の振る舞いが不可解過ぎるのだ。事実、

note that this behavior depends on the application. In Windows, each application parses its own command line parameters. I believe the behavior you’re describing is that of Microsoft’s standard C library,

は全文としては間違っているものの、「each application parses its own command line parameters」の一部は正しい。これがまさしく「cmd.exe が取り払ってくれない二重引用符」など不可解に残る「意図しない入力」の解読はこれはアプリケーションの仕事のままなのであった。Windows ネイティブ版を提供するメジャーな「クロスプラットフォームな OSS」の多くがこれに苦心して対応している。つまり「ステキな cmd.exe から呼ばれましたモード、きゃー」を実現している。

一瞬これまでの検討・検証が全部無駄だったのではないかと焦ったわよ。