しばたテックブログ

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

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)

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