Windowsのコマンドライン引数でのクォートの話

by YAMAMOTO Yuji on December 11, 2021

Tagged as: PowerShell.

この記事は、こちらのページにも同時投稿しています。


この記事は、もともとQrunchというサービスに掲載していた記事ですが、サービスごとなくなっていました。今回、ググって解決しづらかったこと Advent Calendarのテーマにぴったりだと感じたので11日目の記事として再掲致します。

以下、公開当時の内容をそのまま載せます。

ここ数年開発時はPowerShellを使っていて、ずっと困っていたことがありました。

例えばgit commit-mオプションにダブルクォートを渡したくなったとき、PowerShellではエスケープシーケンスにバッククォートを使うとのことなので👇のように書いてみたとします:

> git commit -m "Implement `"Hello, world`" finally!"
error: pathspec 'world finally!' did not match any file(s) known to git

なぜかダブルクォートが適切にエスケープされていないかのようなエラーになってしまいました。

シングルクォートで囲えばいいだろ、と思ってやってみてもやっぱりダメ:

> git commit -m 'Implement "Hello, world" finally!'
error: pathspec 'world finally!' did not match any file(s) known to git

おかしいなぁと悩みながら、なんとなくバッククォートの前にバックスラッシュを付けてみたところ、なんとうまくいくじゃありませんか!

> git commit -m "Implement \`"Hello, world\`" finally!"
On branch master
Your branch is up to date with 'origin/master'.

...

なぜだろうと首をひねっていたところ、Twitterでこんな⬇️情報を教えていただきました:

必要なところをかいつまんで説明しましょう。

Windowsにおいてコマンドを呼び出す最もプリミティブなAPI、— つまり、Windowsで子プロセスを作るあらゆるアプリケーションが間接的に使うAPI —、CreateProcessでは、コマンドライン引数は、文字列の配列ではなく一つの文字列として渡されるそうなのです。
しかし、それでは普通のC言語のアプリケーションを書いたときmain関数のargvが常に2つの文字列(1つめはコマンドの名前ですね)になってしまって不便なので、呼び出されたコマンドがmain関数に渡す前に)自らコマンドライン引数をパースしているというのです!

その際どのようにコマンドライン引数をパースするかは、使用したプログラミング言語、特にCやC++ではVC++ランタイムのバージョンによって異なるそうです😵!

冒頭で紹介したダブルクォートに関するルールも、上記のページにもっと詳しく書かれています。
つまりバッククォートだけでなくバックスラッシュを付ける必要があったのは、PowerShellの仕様ではなく、Windowsで動くコマンド全般に関する挙動だったんですね!
どうりでPowerShellについて解説したページには出てこないワケです。

そして、今回は試してませんが、きっとコマンドプロンプトでも同じ問題にぶち当たるのでしょう。

Windowsではコマンドライン引数はアプリケーションがパースする、という話はどこかで聞き覚えがありましたが、ダブルクォートとかも自前で処理してたんですね😰…

ちなみに、実は同じ原因の問題は、stack test--test-argumentsを使うときにもしばしばぶち当たっていました。
例えば、Hspecで書いたテストのうち、テストの説明にfoo barという(空白を含む)文字列を含むものを実行したいとき:

> stack test --fast --test-arguments "--match `"foo bar`""
Error: While constructing the build plan, the following exceptions were encountered:

Unknown package: bar

Some different approaches to resolving this:


Plan construction failed.

これもgit commitの場合と同様、バッククォートの前にバックスラッシュを付ければ回避できます。

> stack test --fast --test-arguments "--match \`"foo bar\`""

もちろん、ダブルクォートの文字列リテラルの代わりに、シングルクォートの文字列リテラルでダブルクォートの前にバックスラッシュを付けるのでも🆗です:

> stack test --fast --test-arguments '--match \"foo bar\"'

これで完璧!🙌
Windowsめんどくさいね!🏁


I'm a Haskeller Supported By Haskell-jp.