しばたテックブログ

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

Write-HostとWrite-Outputの違い

新年一発目のエントリなのでPowerShellの基本的なことについて書きます。
PowerShellの基本的なことがわかってなかったシリーズ第8弾でもあります。

このエントリを書く動機

Google等でWrite-Host Write-Output 違いなワードで検索すると非常にアレな感じだったのでもう少しまともにしたいというのが動機です。

私自身PowerShell勉強中の身なので大したことは書けませんが、それでもすこしは現状をマシにできると思っています。(本当はもっと詳しい人にこのエントリを書いてもらった方がうれしいのですが...)

前提となる基本

Write-HostWrite-Outputの違いについて触れるまえにPowerShellの重要な基本に触れる必要があります。
それは、

PowerShellはオブジェクトを扱うシェルである。

という点です。

これがどう重要かというと、コマンドプロンプト(cmd.exe)やbash等のUnix系シェルはテキストベースのシェルであり、標準・エラー出力に出力されるテキストとコンソール表示される内容は基本的に同一となります。*1

対してPowerShellでは標準・エラー出力に相当するStandard output stream(1>)Error output stream(2>)にはオブジェクトが出力され、最終的にコンソールにはオブジェクト毎に定められた書式のテキストが表示されます。

このため、PowerShellでは出力と表示が基本的に一致せず、

(各ストリームへの)出力と(コンソールの)表示は常に分けて考える必要がある。

という事になります。

これは、PowerShellに慣れている人にとっては当たり前に感じると思いますが、慣れてない人にとっては意外と意識されていない様に見受けられます。*2

Write-HostとWrite-Outputの違い

この基本を踏まえてWrite-HostWrite-Outputの違いについて触れていきます。
まずは各コマンドレットがどういうものかについて説明します。

Write-Hostついて

Get-Help Write-Hostを実行してヘルプを見ると、概要は、

カスタマイズした出力をホストに書き込みます。

となっています。
ここでいう"ホスト"はPowerShellの実行環境(PowerShellコンソールやPowerShell ISE等)であり、ホスト≒コンソールと考えて差し支えありません。

Write-Hostはコンソールに文字を表示させるだけのコマンドレットです。
Write-Hostを実行してもStandard output streamへの出力はありません。*3
このため出力結果を変数に設定することもできません。

Write-Outputについて

Get-Help Write-Outputを実行してヘルプを見ると、概要には、

指定されたオブジェクトをパイプラインの次のコマンドに送信します。 そのコマンドがパイプラインの最後のコマンドである場合、オブジェクトはコンソールに表示されます。

と書いてあり、説明に、

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

と書いてあります。

Write-Outputは、Standard output stream(="出力ストリーム")に指定のオブジェクトを出力するコマンドレットです。
出力の型がSystem.Management.Automation.PSObjectになっており、文字列に限らずあらゆるオブジェクトを扱います。
Standard output Streamに出力されているので結果を変数に設定することができます。

そして、重要なことなのですが、Write-Output自体はコンソールへの表示を行う機能は持っていません。

ただし、

そのコマンドがパイプラインの最後のコマンドである場合、オブジェクトはコンソールに表示されます。

とある様に、パイプラインの最後*4Write-Outputを実行すると最終的にはコンソールにオブジェクトに応じた文字列が表示されます。
こちらについて詳細は後述します。

Write-HostとWrite-Outputの違い

ここまでの内容をまとめると、

  • Write-Hostはコンソールに文字を表示し、Standard output streamへの出力はしない。
  • Write-OutputはStandard output streamにオブジェクトを出力し、コンソールへの表示は行わない。
    (ただし、後述の仕組みにより最終的にはコンソールに文字が表示される)

となり、表示と出力で明確に目的が分かれていることがわかります。

このため、表示を目的としたWrite-Hostでは-ForegroundColor-BackgroundColorによる色指定や、-NoNewlineによる改行の有無を指定できるものの結果を変数に設定することができず、出力を目的としたWrite-Outputでは結果を変数に設定できますがWrite-Hostの様なパラメーターはありません。

単純に利用する分には似た動作に見えるWrite-HostWrite-Outputですが、その実全くの別物であることがわかります。

既定の出力、Out-Defaultについて

ここからは補足的な内容となり、若干PowerShellのコアな部分について触れていきます。

Write-Outputにはコンソールに文字を表示する機能がないにもかかわらず最終的にはコンソールに文字列が出力されます。

この仕組みについてはTechNetマガジンの以下の記事に詳しく記載されています。

Windows PoweShell: 出力に関するオプション

この記事を読んでもらえば私から説明することは無い気もしますが、かいつまんで説明していきます。

PowerShellの出力の基本

上記の記事にある通り、PowerShellでオブジェクトは必ず原則*5

  1. Format-* コマンドレットにより表示の書式を設定
  2. Out-* コマンドレットにより最終出力

の手順を踏み何らかの表示や出力がなされます。

Format-*なコマンドレットは、

名称 表示形式 特記事項
Format-List リスト形式
Format-Table 表形式
Format-Wide 複数列表示
Format-Custom 独自の表示形式 Update-FormatDataで設定
Format-Default 既定のフォーマット 内部用。詳細は後述

Out-*なコマンドレットは、

名称 出力先 特記事項
Out-Host ホスト PowerShellコンソール、PowerShell ISEなど
Out-File ファイル
Out-GridView グリッド形式のウィンドウ
Out-Null 出力なし >/dev/nullに相当
Out-Printer プリンタ
Out-String 書式設定されたオブジェクトを文字列に変換し出力 このコマンドレットだけは例外的に後続にパイプすることが可能
Out-Default 既定の出力 ≒Out-Host。詳細は後述

があります。

既定の出力、Out-Default

上記の原則によれば、オブジェクトをコンソールに表示するにはOut-Hostコマンドレットを呼び出す必要がありますが、実際には呼び出さなくてもコンソールに文字列が表示されるのは前述のとおりです。

これは、明示的にOut-*を呼ばない場合は既定の出力Out-Defaultが暗黙的に呼ばれているためです。

Out-Defaultとは一体何なのか?というと、私もまだ勉強中でわからない部分が非常に多いです。   ただ、上記の記事によればOut-DefaultOut-Hostにオブジェクトを転送している*6そうなのでOut-Default≒Out-Hostとみなして問題ないでしょう。

このため、Out-*を明示せず単純にWrite-Outputを実行した場合、以下の様な流れで最終的にコンソールに文字が表示されることになります。

# 単純なWrite-Outputの呼び出し
PS C:\> Write-Output "Hello World."# Out-*を明示しない場合はOut-Defaultが暗黙的に呼ばれてる
PS C:\> Write-Output "Hello World." | Out-Default# Out-Default≒Out-Hostなので最終的にコンソールに文字が表示される
PS C:\> Write-Output "Hello World." | Out-Host 
Hello World.

Out-Defaultの確認

暗黙のOut-Defaultの呼び出しを確認するために、以下のファンクションを定義します。

こちらは、Windows PowerShell in Actionに記載されているコードを参考に*7しており、Out-Defaultの呼び出しがあった時にパイプラインへの入力をフックして独自のコードを実行させるものになります。

Out-Defaultをフックしてパイプラインに渡されたオブジェクトの型と値をホストに表示するサンプル

独自のコードは、

# パイプラインに渡されたオブジェクトの型と値をホストに表示
Write-Host "Called Out-Default | Value Type = $($_.GetType())"
Write-Host "Called Out-Default | Input value = $($_)"

の部分で、Out-Defaultが呼び出されたらコンソールにパイプラインから渡された値とその型を表示する様にしています。

そして、このファンクションを定義した上でWrite-Outputを実行すると以下の様な結果となります。

PS C:\> Write-Output "Hello World."
Called Out-Default | Value Type = string
Called Out-Default | Input value = Hello World.
Hello World.

この結果からOut-Defaultが暗黙的に呼び出されていることがわかります。

既定のフォーマット、Format-Defaultについて

前項のコード例では、Format-*を明示しない例を挙げました。
では、Format-*についてもOut-*と同様に暗黙的にFormat-Defaultが呼ばれているのかというと、どうやら違う様です。

こちらについては、参考となるドキュメントは無かったのですがTrace-Commandコマンドレットを使い以下のコードを試してみたところ、

# PowerShell 5.0な環境で実施
Trace-Command -Name * –Expression { Write-Output "Hello world." | Out-Host }
# および
Trace-Command -Name * –Expression { Write-Output "Hello world." | Out-Default }

トレース中に、

# ※Out-Defualtの場合も同様の内容になる
# Out-Hostの呼び出し開始 
ParameterBinding Information: 0 : BIND NAMED cmd line args [Out-Host]

(中略)

# パイプラインから渡された値は "Hello world."
ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Out-Host]
ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.String]
ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
ParameterBinding Information: 0 :     Parameter [InputObject] PIPELINE INPUT ValueFromPipeline NO COERCION
ParameterBinderController Information: 0 :  WriteLine       Adding PipelineParameter name=InputObject; value=Hello world.
ParameterBinding Information: 0 :     BIND arg [Hello world.] to parameter [InputObject]

(中略)

# Format-Defaultの呼び出し開始
ParameterBinding Information: 0 : BIND NAMED cmd line args [Format-Default]
ParameterBinding Information: 0 : BIND POSITIONAL cmd line args [Format-Default]
ParameterBinderController Information: 0 :  WriteLine   CurrentParameterSetName = __AllParameterSets
ParameterBinderController Information: 0 :  WriteLine   CurrentParameterSetName = __AllParameterSets
ParameterBinding Information: 0 : MANDATORY PARAMETER CHECK on cmdlet [Format-Default]
ParameterBinding Information: 0 : CALLING BeginProcessing
# パイプラインから渡された値は "Hello world."
ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Format-Default]
ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.String]
ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
ParameterBinding Information: 0 :     Parameter [InputObject] PIPELINE INPUT ValueFromPipeline NO COERCION
ParameterBinderController Information: 0 :  WriteLine       Adding PipelineParameter name=InputObject; value=Hello world.
ParameterBinding Information: 0 :     BIND arg [Hello world.] to parameter [InputObject]

(後略)

といった結果が表示され、Out-*コマンドレット内部で既定のフォーマットを行うFormat-Defaultが呼び出されることがわかりました。
もちろんFormat-*を明示した場合はFormat-Defaultは呼び出されません。

なお、Format-DefaultMSDNに記載されている様に、

This cmdlet is for internal system use only.

であり、公開されておらずユーザーが利用することはできません。

最後に

とりあえずこんな感じです。

Write-HostWrite-Outputの違い、PowerShellのフォーマットと出力の基本についてわかる範囲で書きました。
極力間違いのない様にしていますが、現在進行形で調査中な部分もあり正直自信の無い部分もあります。

エントリの内容に間違いがあれば随時訂正していきます。

補足

以下に本エントリの内容に関連する補足資料を挙げます。
PowerShellのストリームの詳細については以下のエントリをご覧ください。

stknohg.hatenablog.jp

Format-*によってオブジェクトがどういう書式をとるかについては牟田口先生の以下のエントリをご覧ください。

winscript.jp

Write-HostWrite-Outputの表示のされ方の違いについては以下のエントリをご覧ください。

stknohg.hatenablog.jp

Write-Output自体について補足を追記しましたので以下のエントリもよければご覧下さい。

stknohg.hatenablog.jp


Windows PowerShell in Action

Windows PowerShell in Action

*1:エスケープシーケンスとかは置いておいて...

*2:実際、私はPowerShellを学習しだしてからこの点を意識するのにしばらく時間がかかりました...

*3:正確な話をすると、PowerShell 5.0からWrite-Hostの結果がInformation streamに出力される様になっていますが、本エントリではわかりやすさのためにその点にはあえて触れません

*4:要は後ろにパイプラインをつなげない状態

*5:例えばOut-GridViewにFormat-*したデータを渡すとエラーになります。

*6:PowerShell 5.0の環境でILSpyでOut-Defaultの実装を見た限りだとOut-Hostとは別実装になっており、この辺の仕組みも記事公開時から変わっているっぽいです...

*7:ほぼ丸パクリですが...