Windowsのコマンドライン引数でのクォートの話
by YAMAMOTO Yuji on December 11, 2021
この記事は、こちらのページにも同時投稿しています。
この記事は、もともと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めんどくさいね!🏁