C Sharpens you up

http://qiita.com/yuba に移しつつあります

Windowsのコマンドプロンプトでワンライナーを書こうとすると二重引用符がビョーキ

Windowsコマンドプロンプト(cmd.exe)でコマンド引数に空白を含む文字列を渡したいときは二重引用符でくくります。ここまではよく知られたノウハウ。

では、二重引用符を含む文字列を渡したいときは?、となると大半の人はあやしいはず。「¥でエスケープすればいい?」、いえ、¥がエスケープ文字になるのはUNIX系のシェルです。cmd.exeでのエスケープはどうなるかというとこれが公式情報が出ていない上に、どうもその隠し仕様も相当複雑なようで、こちらの記事

を読んでみても頭がくらくらするのですが、この知見を下敷きに、最近見つけた面白い動作の例をひとつご紹介します。

問題

次のコマンドは、コマンドラインでどう解釈されてどんな出力になるでしょう?

> cmd /c "set aaa=" bbb "&set aaa"
正解

二重引用符がまさかの入れ子で解釈されます。

> cmd /c "set aaa=" bbb "&set aaa"
子プロセスとして呼ばれたcmd.exeにコマンドとして

> set aaa=" bbb "&set aaa

が渡され、&は連続実行ですから

> set aaa=" bbb "
> set aaa

と実行したのと同じこと。結果として、子プロセスcmd.exeの環境変数aaaに「" bbb "」が格納されてそれをすぐ呼び出すわけですから、出力として

" bbb "

が表示されます。

入れ子で解釈される仕組み

まず、最初のコマンドが&という文字を含んでいるのにここで区切られて2つのコマンドの連続実行だと解釈されてしまわない理由です。コマンドプロンプトは&やパイプ(|)、リダイレクション(>,<)文字などコマンドを分割する文字を探すときに、二重引用符で囲われた部分については対象外とします。

最初のコマンドは&文字の前に奇数個の二重引用符があるので、囲われた中であると解釈され、&で区切られません。

次に、区切られないままコマンドが実行されるのですが、コマンドへの引数の渡され方です。Windowsではプロセスに引数は単一の文字列として渡されます。空白文字で区切ったりするのはプロセス側の仕事とされています(といっても、プログラマが区切り処理を書くことは滅多になく、処理系のスタートアップコードに記述されていることがほとんどです。だから、Cならargv, argcの配列に区切られて渡されてきたかのように扱えます)。

区切られないまま子プロセスのcmd.exeに渡される引数は、こう。

/c "set aaa=" bbb "&set aaa"

cmd.exeはこれを頭から読んでいって、スイッチでない文字列が登場したところから最後までをコマンド入力と解釈します。このとき、そのコマンド入力の先頭と末尾が二重引用符で囲われているとはずしてしまいます。この二重引用符外しが結果として入れ子解釈となりました。

set aaa=" bbb "&set aaa
この知識で何ができるか

実行するコマンドにだけ与える一時的な環境変数を設定したいときに使えます。
今回この仕様に気付いたのは、VSSのバージョン履歴をGitに移行するプログラムを書いていてです。git commitコマンドでコミット名義、コミット日時を指定するには環境変数を設定する必要があったというシチュエーションでした。