しばたテックブログ

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

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を呼び出す場合は注意してみてください。

Write-Outputとはいったい何なのか?

以前に書いた

stknohg.hatenablog.jp

に対するさらなる補足です。
本当はもう少し早い時期に書きたかったのですがなかなか書けずにおりました...

別にWrite-Outputを使わなくても...

先のエントリで触れた様にWrite-Output

説明
Write-Output は、"出力ストリーム" や "正常終了パイプライン" とも呼ばれるプライマリ パイプラインにオブジェクトを送信します。

と、オブジェクトを"出力ストリーム"(1>)に送るだけのコマンドレットです。

しかしながら、PowerShellでは別にWrite-Outputを使わずとも変数やリテラルを定義・評価するだけでそのオブジェクトは"出力ストリーム"(1>)に送られます。

極端な例を出すとコンソールに"Hello World!"と打つだけで文字列は評価されてストリームに乗り、最終的には以下の様にコンソールに文字列が表示されることになります。

# PowerShellではWrite-Outputを使わずとも評価されるだけでそのオブジェクトは出力ストリーム(1>)に乗り、
# 最終的にコンソールに表示される。
PS C:\> "Hello World!"
  ↓
PS C:\> "Hello World!" | Out-Default
  ↓
PS C:\> "Hello World!" | Out-Host
Hello World!

ですのでWrite-Outputを使わなくても同様のことは普通にできてしまいます。

Write-Outputは何のために存在しているのか?

ではWrite-Outputは何のために存在しているのでしょうか?

いろいろ調べてみた結果、いくつかの候補は思いついたのですが、これだという結論にたどり着くことができませんでした。
以下その候補について書いていきます。

1. ストリームへの出力を統一するためのWrite-Output

stknohg.hatenablog.jp

上のエントリでも述べていますがPowerShellのストリームは出力ストリーム(1>)~Information Stream(5>)とProgress Streamがあり、それぞれのストリームにオブジェクトを出力するために、

  • Write-Output
  • Write-Error
  • Write-Warning
  • Write-Verbose
  • Write-Debug
  • Wirte-Information
  • Write-Progress

コマンドレットが存在しています。

各ストリームへの出力方法の統一のためにWrite-Outputが存在しているというのが一番合理的な説明ができる気がします。

個人的には、初めにWrite-Outputがあり、利便性のために出力ストリーム(1>)への出力だけはWrite-Outputが無くても良い様にしたのではないかと予想しています。

これが一番の理由なのではないかと思っているのですが明確な根拠を得ることはできませんでした...
あくまで予想にすぎませんのでその点はご注意ください。

2. echoコマンドとしてのWrite-Output

PowerShellでは既定でechoWrite-Outputのエイリアスとして定義されています。
以下のコマンドでエイリアスの実体を確認することができます。

PS C:\> Get-Alias echo

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           echo -> Write-Output

コマンドプロンプト等の他のシェルの標準出力に対応するのが出力ストリームであるためWrite-HostでなくWrite-Outputがエイリアスの実体になるのは妥当だと思います。

ちょっと逆説的ですが、PowerShellにおけるechoコマンドの役割を果たすためにWrite-Outputが存在していると言えなくもありません。
こちらも予想に過ぎず根拠もかなり弱い感じです...

3. -NoEnumerateのためのWrite-Output

これは存在理由というよりはWrite-Outputのもう一つの役割と言ったほうが正確ですね。

PowerShellのパイプラインは配列等の"Enumerable"なオブジェクトが渡された場合、その要素ごとに順次パイプラインを流れていく挙動となります。

例えば以下の様に@(1..100)の配列をパイプラインでつなげると、後続のForEach-Objectには配列の各要素のint型の値が順に渡され、Processブロックは100回呼び出されることになります。

PS C:\> Write-Output @(1..100) | ForEach-Object -Begin { $Counter = 0 } -Process { $Counter += 1 } -End { Write-Host "$Counter 回呼ばれました!" }
100 回呼ばれました!

Write-Outputには-NoEnumerateというパラメーターが存在しており、このパラメーターを指定した場合はパイプラインに要素ごとではなく配列そのものを渡す挙動になります。

先程の例に対し-NoEnumerateを付けてパイプラインでつなげると、後続のForEach-Objectにはint[]型の配列がそのまま渡され、Processブロックの呼び出しは1回だけになります。

PS C:\> Write-Output @(1..100) -NoEnumerate | ForEach-Object -Begin { $Counter = 0 } -Process { $Counter += 1 } -End { Write-Host "$Counter 回呼ばれました!" }
1 回呼ばれました!

Write-Outputを使わない場合は-NoEnumerateと同じ挙動をさせることはできません。
このためにWrite-Outputが存在しているというのは...流石に言い過ぎですね...
(追記あり)

【2016/03/26追記】

カンマ単項演算子のことを完全に失念していました。
これを使えばWrite-Outputを使わなくとも-NoEnumerateと同じ挙動をさせることができますね...Orz

PS C:\> ,@(1..100) | ForEach-Object -Begin { $Counter = 0 } -Process { $Counter += 1 } -End { Write-Host "$Counter 回呼ばれました!" }
1 回呼ばれました!

言い過ぎどころかただの間違いでしたね。まあ、Write-Outputにはこういうパラメーターもあるよという事でお許しください。

Write-Outputを明示して使うべきか?

結局Write-Outputは何のために存在しているかという問いには答えることは出来なかったのですが、答えがわからなくても実害は無いので大きな問題にはなりません(
あまり深く考えないほうが幸せなPowerShellライフを過ごせるんじゃないかと思います。

ただ、存在理由がぼんやりしているせいで、PowerShellを利用する際にWrite-Output明示して使うべきかどうかという点は非常に悩ましいです。
-NoEnumrateを使う場合はWrite-Oututを明示しなければなりませんが、それ以外の場合については公式にガイドラインがあるわけでもないため、正直なところ、答えのでない問いだと思っています。

私個人の思いとしてはWrite-Outputは可能な限り明示した方が良いと考えています。
これは、PowerShellにある程度慣れていないとWrite-Outputを明示しない場合でストリームに出力されるコードとされないコード(例えば値の代入は評価した結果を出すわけではないのでストリームには出力されません)を区別するのは難しいと思うからです。

ちょっと極端な例ですが、例えば以下の様なコードがあった場合、PowerShellに慣れていないと何が出力されるかすぐにはわからないと思います。

$i = 0
++$i
(++$i)

これに対してWrite-Outputを明示してやるとかなりわかりやすくなると思います。

$i = 0
++$i
Write-Output (++$i)

最終的にどちらが良いかの判断はみなさんに委ねます。

Get-DateはGet-DateであってNew-Dateではないお話し

最近ブログを書く気力が薄れてしまっているので軽いネタでリハビリします。
小ネタがいくつかたまっているので何とか消化したいですね...

New-Object System.Datetime と Get-Date の違い

先ずはこちらのコードをご覧ください。

PS C:\> $Date1 = New-Object System.Datetime @(2016, 3, 22)
PS C:\> $Date2 = Get-Date -Year 2016 -Month 3 -Day 22

上のコードで取得できる$Date1$Date2は同じ値になると思いますか?

$Date1は年、月、日を指定するコンストラクタでSystem.DateTimeの値を生成します。
$Date2Get-Dateコマンドレットで年、月、日を指定してSystem.DateTimeの値を取得しています。

実際に比較すると結果はFalseを返し二つの値は異なることがわかります。

PS C:\> $Date1 -eq $Date2
False

どちらも本日(2016/03/22)の値を取得していますが、もう少し詳しく見てみると、$Date1は年月日以外は初期値なのですが、$Date2は年月日以外に現在時刻が設定されています。

PS C:\> $Date1.ToString("yyyy/MM/dd HH:mm:ss.fff")
2016/03/22 00:00:00.000
PS C:\> $Date2.ToString("yyyy/MM/dd HH:mm:ss.fff")
2016/03/22 20:01:27.973

Get-DateはGet-DateであってNew-Dateでは無いお話し

Get-Dateの仕様について

私はずっと上記例の$Date1$Date2は同じ値を返すものだと思い込んでいました。
ただ、各パラメーターのヘルプを見てみると確かに、

-Year
表示する年を指定します。1 ~ 9999 の値を入力します。既定値は現在の年です。


-Month
表示する月を指定します。1 ~ 12 の値を入力します。既定値は現在の月です。


-Day
表示されている月の日にちを指定します。1 ~ 31 の値を入力します。既定値は現在の日付です。


-Hour
表示する時を指定します。1 ~ 23 の値を入力します。既定値は現在の時です。


-Minute
表示する分を指定します。1 ~ 59 の値を入力します。既定値は現在の分です。


-Second
表示する秒を指定します。1 ~ 59 の値を入力します。既定値は現在の秒です。


-Millisecond
日付のミリ秒を指定します。0 ~ 999 の値を入力します。既定値は現在のミリ秒数です。

と既定値が「現在の○○です。」になっており、現在時刻の値を設定する様になっていました。

Get-DateはGet-DateであってNew-Dateではないお話し

私見を述べれば何故こんなわけのわからない仕様にしたのだという気持ちでいっぱいです。

PowerShellでは$Date2の様にNew-ObjectDatetime型の値も生成できるので上記の例が同じ値になる方が直感的であり統一性もあると思っています。

中の人たちがどの様な意図でこの仕様にしたのか断定できる資料は無いのですが、推測すると、Get-Dateの概要には、

概要
現在の日付と時刻を取得します。

とあるので、このコマンドレットはあくまでも"現在時刻を取得する"ものであり"時刻を生成するものではない"という事なのでしょう...
不満で仕方ありませんがそういうものだと割り切ることにします。

New-Dateを作ってみた

ただ、何もせずにこの仕様に従うのが悔しかったので極力Get-Dateと互換のあるNew-Dateを作ってみました。
以下の様なコードになります。

新規にDatetime型のオブジェクトを生成するコマンド

こいつを使えば上記の例も、以下の様に同じ値を返す様になります。

PS C:\> $Date1 = New-Object System.Datetime @(2016, 3, 22)
PS C:\> $Date2 = New-Date -Year 2016 -Month 3 -Day 22

PS C:\> $Date1 -eq $Date2
True

実際の運用ではこのファンクションを使うことは無いとは思いますが*1参考までにという事で。

補足 もうひとつのNew-Object System.Datetime と Get-Date の違い

補足として、New-Objectを使った場合とGet-Dateを使ってSystem.Datetime型の値を取得した場合のもう一つの違いについて説明します。

前項のNew-Dateのコード内で軽くネタバレしているのですが、Get-Dateで取得したSystem.Datetime型の値にはDisplayHintNotePropertyが付与されています。
最初の例の結果に対してそれぞれGet-Memberしてやると、

PS C:\> $Date1 | Get-Member -View Extended


   TypeName: System.DateTime

Name     MemberType     Definition
----     ----------     ----------
DateTime ScriptProperty System.Object DateTime {get=if ((& { Set-StrictMode -Version 1; $this.DisplayHint }) -ieq  "...


PS C:\> $Date2 | Get-Member -View Extended


   TypeName: System.DateTime

Name        MemberType     Definition
----        ----------     ----------
DisplayHint NoteProperty   DisplayHintType DisplayHint=DateTime
DateTime    ScriptProperty System.Object DateTime {get=if ((& { Set-StrictMode -Version 1; $this.DisplayHint }) -ieq...

Get-Dateで得た方の値にのみDisplayHintプロパティが付いている事がわかります。

このDisplayHintプロパティはコンソールにSystem.Datetime型の値を表示する際のフォーマットをつかさどり、既定値はDatetimeで日付と時刻がコンソールに表示されます。

このプロパティを変えるとコンソールに表示される内容が変化します。

# DisplayHintプロパティの既定値は DateTime
PS C:\> $Date2.DisplayHint
DateTime

# 既定ではコンソールには日付と時刻が表示される
PS C:\> $Date2

20163220:00:00

# DisplayHintの値を変えるとコンソールに表示される内容も変わる
PS C:\> $Date2.DisplayHint = [Microsoft.PowerShell.Commands.DisplayHintType]::Date
PS C:\> $Date2

2016322

最後に

とりとめのない感じになりましたがとりあえずこんな感じです。
Get-Dateを使う際はちょっとだけ注意してみてください。

*1:作った本人もこんなものをいちいち使う気はないですw