しばたテックブログ

気分で書いている技術ブログです。

powershell.exe -Commandパラメーターの謎挙動について


【2016/03/30追記】

謎挙動の原因がわかりましたので追記エントリを書きました。

stknohg.hatenablog.jp

こちらも併せてご覧ください。


きっかけはQiitaのこのエントリ。

qiita.com

対応方法を調べてコメントする際に謎挙動に気が付きました。

何が謎挙動なのか?

通常PowerShellで特殊文字をエスケープする際はバッククオート`を使います。

ダブルクオート"をエスケープする場合は`"と記述します。

このダブルクオート"ですが、PowerShellコンソール上でpowershell.exeを起動して-Commandパラメーターを指定した場合に限っては\`"とエスケープする必要があります。

正直謎の挙動です。だれかこれについて知ってたら教えてください。
この挙動に関してヘルプ等を探してみたのですが明記されている部分を見つけることが出来ませんでした。

挙動を確認してみる

簡単なサンプルでこの挙動を試します。
PowerShell 5.0な環境で試してますが他のバージョンでも同様の動作をします。

実行サンプル

以下のコードをpowershell.exeから起動して呼び出してみます。

$word='PowerShell'; Write-Output "Windows $word"

コンソール上でこのコードを実行すると以下の様に"Windows PowerShell"と文字列を返して終わります。

PS C:\> $word='PowerShell'; Write-Output "Windows $word"
Windows PowerShell

特に悩むことのない簡単なコードです。

`" でエスケープした場合

最初に挙動を確認するために`"でエスケープした場合を試します。
実行するのは以下のコードとなります。

# PowerShellコンソールからさらにPowerShellを起動
# "以外にも$も特殊文字なので`でエスケープしている
powershell.exe -Command "`$word='PowerShell'; Write-Output `"Windows `$word`""

このコードを実行すると結果は下図の様になり、"Windows""PowerShell"2つの文字列に分かれてしまっていることがわかります。

f:id:stknohg:20160329185127p:plain

これは、

$word='PowerShell'; Write-Output Windows $word

の様にWrite-Outputに付ける"が消失してWindows$Wordの2引数が与えられた扱いとなり、string[]の配列が戻り値になってしまったために起きています。

当然ですがこれは期待した動作ではありません。

\`" でエスケープした場合

次に\`" とエスケープした場合を試してみます。
コードは以下。

# PowerShellコンソールからさらにPowerShellを起動
# -Command内の文字列で " をエスケープするには \`" とする必要がある
powershell.exe -Command "`$word='PowerShell'; Write-Output \`"Windows `$word\`""

このコードを試すと下図の結果となり、正しく"がエスケープされて期待した動作となります。

f:id:stknohg:20160329185438p:plain

-Commandパラメーター以外でのエスケープについて

最初に言った様にpowershell.exe -Commandパラメーター以外ではダブルクオートは`"で問題なくエスケープできます。

簡単な例としてPowerShellコンソールからコマンドプロンプトを起動する場合で試してみます。
コードは以下。
単純にコマンドプロンプトからdirコマンドを呼んでいるだけです。
呼び先のディレクトリにスペースが入っているので`"でエスケープしています。

# PowerShellコンソールからコマンドプロンプトを起動
# この場合は通常の `" だけでエスケープできる
cmd.exe /C "dir `"C:\Program Files\Common Files`""

実行結果は以下の様になり、\`"としなくても問題ありません。

f:id:stknohg:20160329190113p:plain

補足1 コマンドプロンプトからPowerShellを呼び出した場合

これまでの内容と直接関連しませんが、PowerShellコンソールでなくコマンドプロンプトからサンプルを呼び出す場合について触れます。

コマンドプロンプトで"をエスケープするには""とする必要があります。
ですのでサンプルを動作させるには以下の様に記述します。

REM コマンドプロンプトからPowerShellを起動する場合
powershell.exe -Command "$word='PowerShell'; Write-Output ""Windows $word"""

結果は下図の通りです。

f:id:stknohg:20160329191041p:plain

特に注意する点もない感じです。

補足2 --%を使った回避策

この謎挙動を解析停止記号(stop-parsing symbol)--%を使って回避することが可能です。
解析停止記号についてはabout_Parsingか以下の記事を参考にしてください。

www.atmarkit.co.jp

簡単に言うとこれは--%以降の引数の解釈をコマンドプロンプトと同等にする機能になります。
先のサンプルは--%を使うと以下のコードで実行可能です。

# PowerShellコンソールからさらにPowerShellを起動
# --% を使って以降の引数の解釈をコマンドプロンプトと同様にしている。
powershell.exe --% -Command "$word='PowerShell'; Write-Output ""Windows $word"""

結果は以下の通り期待通りの動作となります。

f:id:stknohg:20160329192502p:plain

最後に

地味にハマりそうな挙動です。
PowerShellコンソールからpowershell.exeを呼び出す場合は注意してみてください。