Write-Hostをやめる ── PowerShellの出力ストリームとログ設計

· · PowerShell, Windows, ログ, 運用改善, 自動化, スクリプト, 設計, 保守性

更新履歴(初版のみ・2026年07月25日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547443)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Write-Hostをやめる ── PowerShellの出力ストリームとログ設計」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547443

DOI(最新版)
10.5281/zenodo.21547443
DOI(この版)
10.5281/zenodo.21547444

「スクリプトは動いているが、失敗したときに何が起きたのか分からない」── 運用に載せたPowerShellスクリプトで最も多い相談がこれです。そして原因を追うと、たいてい同じ構造に行き当たります。処理の状況が Write-Host でしか表現されておらず、画面を見ていなかった夜間実行では何も残っていない、というものです。

PowerShellには6種類の出力ストリームがあり、それぞれ「誰に向けた情報か」が違います。値を返すのか、人間に読ませるのか、調査時だけ必要なのか。これを意識して書き分けると、同じスクリプトが対話実行では親切に、無人実行では機械可読に振る舞うようになります。逆に全部を Write-Host に流し込むと、値としても使えず、ログにも残らない情報だけが増えます。

この記事では、6つのストリームの役割、Write-Host の正しい位置づけ、関数の戻り値が汚れる仕組み、呼び出し側から詳細度を制御する方法、そして運用で使える構造化ログの型までを整理します。

1. まず結論

  • PowerShellの出力ストリームは6つ。成功(1)・エラー(2)・警告(3)・詳細(4)・デバッグ(5)・情報(6)で、それぞれ番号でリダイレクトできます。*> は全ストリームです。1
  • PowerShell 5.0以降、Write-Host は情報ストリームに書き込みます。そのため 6> でのリダイレクトや -InformationVariable での捕捉が可能になりました。それ以前は捕捉も抑止もできませんでした。2
  • Write-Host は「人間に見せる表示」専用です。値を返す用途には使えません。パイプラインに渡す値は Write-Output(または裸の出力)です。23
  • 関数は、内部で出力されたすべてのオブジェクトを返します。return の有無は関係ありません。不要な出力は $null = ... で捨てるのが定石です。4
  • 進捗は Write-Progress、処理の経過は Write-Verbose進捗表示はリダイレクトできるデータストリームではありません。5
  • [CmdletBinding()] を付けた関数は、-Verbose -Debug -InformationAction などの共通パラメーターを自動で持ちます。呼び出し側が詳細度を決められる形にするのが正解です。67
  • 既定値は覚えておく価値があります。$VerbosePreference $DebugPreference $InformationPreference はSilentlyContinue、$WarningPreference $ErrorActionPreference はContinueです。8
  • 後から解析したいログは構造化(1行1JSON)します。画面の見た目を再現したいなら Start-Transcript を併用します。9

2. 6つのストリームと、その相手

# ストリーム 書き込むコマンド 想定読者 既定の環境設定
1 成功(Success) Write-Output / 裸の出力 後続の処理(パイプライン)
2 エラー(Error) Write-Error / スロー 人間 + 監視 Continue
3 警告(Warning) Write-Warning 人間 Continue
4 詳細(Verbose) Write-Verbose 調査中の人間 SilentlyContinue
5 デバッグ(Debug) Write-Debug 開発者 SilentlyContinue
6 情報(Information) Write-Information / Write-Host 人間 + 記録 SilentlyContinue

この表で最も重要なのは1行目です。成功ストリームは「人間向けのメッセージ」を書く場所ではありません。ここに人間向けの文字列を書くと、その関数を | でつないだ瞬間に、後続の処理へ想定外の文字列が流れ込みます。

function Get-KsTargetFile {
    Write-Output "対象を検索しています..."   # 【NG】戻り値に混ざる
    Get-ChildItem -Path $path -Filter '*.csv'
}

# 呼び出し側は FileInfo の配列を期待しているのに、先頭に文字列が入ってくる
$files = Get-KsTargetFile
$files[0].FullName    # → 空(先頭は文字列だから)

正しくは、経過報告は詳細ストリームか情報ストリームへ送ります。

function Get-KsTargetFile {
    [CmdletBinding()]
    param([string] $Path)

    Write-Verbose "対象を検索しています: $Path"   # -Verbose 指定時だけ表示される
    Get-ChildItem -Path $Path -Filter '*.csv'      # 戻り値は FileInfo だけ
}

3. Write-Hostは「悪」なのか

かつて「Write-Hostは使うな」という主張が広まりましたが、現在のPowerShellでは事情が変わっています。PowerShell 5.0以降、Write-Host は情報ストリーム(6番)への書き込みとして実装されており6> でリダイレクトしたり -InformationVariable で捕捉したりできます。2 「画面にしか出せず、後から一切拾えない」という当時の批判は、もう当てはまりません。

とはいえ、使いどころは限定されます。

Write-Hostが向いている場面

  • 対話的に使うツールで、色付きの見出しや区切りを出したいとき(-ForegroundColor)
  • 「このスクリプトはこれから何をするか」をユーザーに伝えるとき
  • 処理の値ではなく、装飾された表示そのものが目的のとき

Write-Hostを使ってはいけない場面

  • 関数の戻り値として値を渡したいとき(→ Write-Output)
  • 後で解析する運用ログを残したいとき(→ 構造化ログ、または Write-Information)
  • 呼び出し側で表示・非表示を切り替えたいとき(→ Write-Verbose)

無人実行のスクリプトでは、そもそも表示先がありません。Write-Host だけで状況を表現したスクリプトは、タスクスケジューラから実行された瞬間に「何も分からないスクリプト」になります。ここが、この記事のタイトルの意味です。

4. 呼び出し側に制御を渡す ── [CmdletBinding()]と共通パラメーター

Write-Verbose の真価は、表示するかどうかを呼び出し側が決められる点にあります。関数に [CmdletBinding()] を付けるだけで、-Verbose -Debug -WarningAction -InformationAction -ErrorAction などの共通パラメーターが自動的に使えるようになります。67

function Invoke-KsImport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $CsvPath
    )

    Write-Verbose "取り込み開始: $CsvPath"          # 既定では出ない
    Write-Information "取り込み: $CsvPath" -Tags 'KsImport'   # 既定では出ない(捕捉は可能)

    $rows = Import-Csv -Path $CsvPath
    if ($rows.Count -eq 0) {
        Write-Warning "$CsvPath に取り込み対象がありません"   # 既定で出る
        return
    }

    Write-Verbose "$($rows.Count) 件を処理します"
    $rows | ForEach-Object { ConvertTo-KsRecord $_ }         # 戻り値はこれだけ
}

# 通常実行: 警告のみ表示され、戻り値はレコード
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv'

# 調査時: 経過も見たい
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv' -Verbose

# 情報ストリームだけを変数に捕まえて、あとでログファイルへ
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv' -InformationVariable info
$info | ForEach-Object { $_.MessageData } | Add-Content -Path $logPath

自前の $LogLevel 変数を作ってif文で分岐する実装をよく見かけますが、標準の仕組みに乗せたほうが短く、他人にも意図が伝わります-Verbose を付ければ詳細が出る、というのはPowerShellを使う人全員の共通認識だからです。

なお、$VerbosePreference などの環境設定変数はスコープと子スコープに効きます。8 関数を -Verbose 付きで呼ぶと、その中から呼ばれるコマンドレットも詳細出力を始めるため、想定より出力が増えることがあります。

5. 関数の戻り値が汚れる ── PowerShell特有の落とし穴

PowerShellの関数は、明示的な return がなくても、内部で出力されたすべてのオブジェクトを返します4 これは強力な仕様である一方、最も事故が多い部分でもあります。

function New-KsWorkFolder {
    param([string] $Path)

    New-Item -Path $Path -ItemType Directory   # 【罠】DirectoryInfo が戻り値に混ざる

    $list = [System.Collections.Generic.List[string]]::new()
    $list.Add('log')                            # 【罠2】.Add() は void なので実害なし
    $sb = [System.Text.StringBuilder]::new()
    $sb.Append('x')                             # 【罠3】StringBuilder 自身が返る

    return $Path
}

$p = New-KsWorkFolder -Path 'D:\work'   # $p は3要素の配列になる(DirectoryInfo, StringBuilder, string)

対処は「不要な出力を捨てる」ことです。書き方は3通りありますが、$null = ... が最も軽量です。

$null = New-Item -Path $Path -ItemType Directory   # 推奨
New-Item -Path $Path -ItemType Directory | Out-Null # パイプの分だけ遅い
[void] $sb.Append('x')                             # .NETメソッド向けによく使われる

この挙動は、テストを書くと一発で気づけます。「戻り値の形を固定する」テストの重要性は「PesterによるPowerShellのテスト整備」で扱っています。

6. リダイレクトと捕捉

ストリームは番号で個別にリダイレクトできます。1

.\Invoke-NightlyExport.ps1 3> warnings.log            # 警告だけ別ファイルへ
.\Invoke-NightlyExport.ps1 4>&1 | Tee-Object -FilePath run.log   # 詳細を成功ストリームに合流
.\Invoke-NightlyExport.ps1 *> all.log                 # 全ストリームを1ファイルへ
.\Invoke-NightlyExport.ps1 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }

> は上書き、>> は追記です。ただし、リダイレクトで合流させると型が混ざる点は意識してください。上の例のようにPowerShellスクリプトや関数のエラーストリームを合流させた場合、その要素はErrorRecordのままなので、上のように型で仕分けられます。

一方で、外部プログラム(ネイティブコマンド)の 2>&1 は事情が違います。PowerShell 7.4以降はリダイレクト出力がバイトストリームとして扱われ、合流後は文字列になるため、ErrorRecordでの仕分けは効きません。外部コマンドのstdout/stderrを区別したい場合は、合流させずに別々に受け取ってください(「PowerShellから外部exeを正しく呼ぶ」)。

実行証跡を丸ごと残したいなら Start-Transcript が手軽です。セッションのコマンドと出力をテキストに記録するので、「そのとき画面に何が出ていたか」を後から再現できます。9

Start-Transcript -Path "C:\Logs\export_$(Get-Date -f yyyyMMdd_HHmmss).log" -Append
try   { Invoke-KsExport }
finally { Stop-Transcript }

7. 後から解析できるログにする ── 構造化ログ

人間が読むログと、機械が集計するログは別物です。「先月このエラーが何件出たか」を知りたいとき、自由書式のテキストは grep との根競べになります。1行1JSON(JSON Lines) で書いておけば、集計はPowerShellだけで完結します。

function Write-KsLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('INFO','WARN','ERROR')] [string] $Level,
        [Parameter(Mandatory)] [string] $Message,
        [hashtable] $Data,
        [string] $Path = $script:KsLogPath
    )

    $entry = [ordered]@{
        ts      = (Get-Date).ToString('o')    # ISO 8601。ソートも突合もしやすい
        level   = $Level
        message = $Message
        script  = $MyInvocation.ScriptName
        host    = $env:COMPUTERNAME
        user    = $env:USERNAME
    }
    # 追加情報は data 配下に入れる。トップレベルに混ぜると、呼び出し側が
    # level や user といったキーを渡したときに基本項目を上書きしてしまう
    if ($Data) { $entry['data'] = $Data }

    # -Compress で1行にする。追記は Add-Content(UTF-8)
    $entry | ConvertTo-Json -Compress -Depth 5 | Add-Content -Path $Path -Encoding utf8

    # 人間向けの表示は標準の仕組みに乗せる(画面には出しても値は返さない)
    switch ($Level) {
        # -ErrorAction は指定しない。ここで固定すると、呼び出し側が
        # -ErrorAction Stop で終了エラーにしたくてもできなくなる
        'ERROR' { Write-Error   $Message }
        'WARN'  { Write-Warning $Message }
        default { Write-Verbose $Message }
    }
}

# 使用例
Write-KsLog -Level INFO -Message '取込完了' -Data @{ rows = 1250; file = 'orders.csv'; ms = 4210 }
# → {"ts":"...","level":"INFO","message":"取込完了","script":"...","host":"...","user":"...",
#    "data":{"rows":1250,"file":"orders.csv","ms":4210}}

集計はこうなります。

Get-Content 'C:\Logs\ks.log' |
    ForEach-Object { $_ | ConvertFrom-Json } |
    Where-Object { $_.level -eq 'ERROR' -and [datetime]$_.ts -ge (Get-Date).AddDays(-30) } |
    Group-Object message | Sort-Object Count -Descending | Select-Object Count, Name

Windowsの標準的なログ基盤(イベントログ・ETW)に載せる選択肢もあります。監視ツールとの連携や、複数台からの収集を考えるならそちらが有利です。設計の比較は「Windowsのイベントログ・ETWと構造化ログ」に、ログファイルの世代管理は「PowerShellスクリプト応用 ── ログ調査・アーカイブ・レポート化」にまとめています。

8. 進捗表示の扱い

Write-Progress はホストの進捗表示機能を使うもので、リダイレクト可能なデータストリームではありません5 つまりログに残せません。無人実行では表示先がないうえ、環境によっては進捗更新のコストが無視できない場合もあります。

# 無人実行スクリプトの冒頭で進捗表示を止める
$ProgressPreference = 'SilentlyContinue'

進捗を運用ログに残したいなら、節目だけを詳細ストリームに書くのが実用的です。

$i = 0
foreach ($row in $rows) {
    $i++
    if ($i % 100 -eq 0) { Write-Verbose "$i / $($rows.Count) 件完了" }
    ...
}

9. 実務の定石(判断表)

出したい情報 使うもの 理由
後続処理に渡す値 Write-Output / 裸の出力 成功ストリームはデータ専用3
処理の経過(調査時だけ見たい) Write-Verbose 呼び出し側が -Verbose で制御7
運用として記録したい出来事 Write-Information + 構造化ログ -InformationVariable で捕捉可能2
対話ツールの装飾表示 Write-Host 情報ストリーム経由なので捕捉も可能2
想定内だが注意すべき事象 Write-Warning 既定で表示され、-WarningVariable で捕捉8
失敗 Write-Error / throw エラー処理は専用記事を参照
開発中の内部状態 Write-Debug -Debug を付けたときだけ7
進捗 Write-Progress(対話時のみ) ログには残らない。無人実行では止める5
実行証跡をまるごと Start-Transcript 独自ログの保険として併用9

10. まとめ

  • PowerShellの出力は6ストリームに分かれています。成功ストリームはデータ専用で、人間向けメッセージを混ぜると戻り値が壊れます。
  • PowerShell 5.0以降の Write-Host は情報ストリームに書くため捕捉可能ですが、値を返す用途と運用ログの用途には使えません。
  • 関数は内部で出力されたすべてを返します。不要な出力は $null = ... で捨てるのが定石です。
  • [CmdletBinding()] を付けて Write-Verbose / Write-Information を使えば、詳細度の制御を呼び出し側に渡せます。自前のログレベル変数より短く、意図が伝わります。
  • 後から集計するログは1行1JSONの構造化ログにします。画面の再現が目的なら Start-Transcript を併用します。
  • 進捗表示はデータストリームではないためログに残りません。無人実行では $ProgressPreference = 'SilentlyContinue' で止めてしまうのが実用的です。

サンプルコードのダウンロード

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。1行1JSONの構造化ログと、戻り値を汚さない関数の書き方が入っています。

サンプルコードをダウンロード(zip)

この記事のサンプルは、PowerShell 7.6 で実際に実行して検証しています(Pester 14件)。zipに含まれる Invoke-SampleTests.ps1 を実行すれば、お手元でも同じ検証を再現できます。

# 構文解析 + 静的解析 + Pesterテスト
./Invoke-SampleTests.ps1

設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、運用スクリプトのログ設計の見直し、「失敗した理由が分からない」状態の解消、監視・集計につながるログ基盤の整備を扱っています。

参考リンク

  1. Microsoft Learn, about_Redirection. PowerShellが成功・エラー・警告・詳細・デバッグ・情報の各ストリームを持ちそれぞれ番号で識別されること、> >> によるファイルへのリダイレクト、n>&1 による他ストリームへの合流、*> による全ストリームのリダイレクトについて。  2

  2. Microsoft Learn, Write-Host. PowerShell 5.0以降のWrite-HostがWrite-Informationのラッパーとして情報ストリームへ出力するようになり、6>によるリダイレクトが可能になったこと、-ForegroundColor / -BackgroundColorによる装飾、出力がパイプラインに渡されないことについて。関連してWrite-Informationは情報ストリームへの明示的な書き込みと-Tagsによる分類について。  2 3 4 5

  3. Microsoft Learn, Write-Output. オブジェクトを成功ストリーム(パイプライン)へ送ること、明示的に呼ばなくても式の結果が同じように出力されることについて。  2

  4. Microsoft Learn, about_Return. PowerShellの関数がreturnの有無にかかわらず、関数内で出力されたすべてのオブジェクトを呼び出し元へ返すこと、returnは値を返しつつ現在のスコープを抜けるための構文であることについて。  2

  5. Microsoft Learn, Write-Progress. コマンドの進行状況をホストの進捗表示として出力すること、$ProgressPreferenceで表示を制御できること、PowerShell 7.4以降は-ProgressAction共通パラメーターでも制御できることについて。  2 3

  6. Microsoft Learn, about_Functions_CmdletBindingAttribute. [CmdletBinding()]属性を付けた高度な関数がコンパイル済みコマンドレットと同様に動作し、共通パラメーターが自動的に利用可能になることについて。  2

  7. Microsoft Learn, about_CommonParameters. -Verbose / -Debug / -WarningAction / -InformationAction / -ErrorAction および対応する -*Variable パラメーターの動作、環境設定変数との関係について。  2 3 4

  8. Microsoft Learn, about_Preference_Variables. $VerbosePreference・$DebugPreference・$InformationPreferenceの既定値がSilentlyContinue、$WarningPreferenceと$ErrorActionPreferenceの既定値がContinueであること、$ProgressPreferenceによる進捗表示の制御、これらがスコープと子スコープに適用されることについて。  2 3

  9. Microsoft Learn, Start-Transcript. セッションのコマンドとコンソール出力をテキストファイルに記録すること、-Appendによる追記、Stop-Transcriptによる停止について。  2 3

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Write-Hostは使ってはいけないのですか?
禁止ではなく、用途が限られるという理解が正確です。PowerShell 5.0以降のWrite-Hostは情報ストリーム(6番)に書き込むため、6>によるリダイレクトや-InformationVariableでの捕捉ができるようになりました。ただし既定では常に画面に表示される前提のコマンドであり、値をパイプラインに流したいときには使えません。対話的なツールで人間に読ませる装飾表示ならWrite-Host、処理の進捗や補足情報ならWrite-VerboseやWrite-Information、後続処理に渡す値ならWrite-Output(または裸の出力)、という使い分けになります。
関数の戻り値に、意図しない値が混ざってしまいます。
PowerShellの関数は、明示的なreturnがなくても、関数内で出力されたすべてのオブジェクトを返すためです。New-ItemやStringBuilderの.Append()など、値を返すコマンドやメソッドを呼び捨てにすると、その戻り値が成功ストリームに流れて呼び出し元に届きます。逆に、List[T]の.Add()のように戻り値がvoidのメソッドは何も出力しないので、抑止は不要です。対処は、不要な出力を$null = ...で捨てるか、| Out-Nullを付けるか、[void]でキャストすることです。性能面では$null = ...が最も軽い書き方になります。
スクリプトの詳細ログを、実行時に切り替えられるようにしたいです。
処理の途中経過をWrite-Verboseで書き、関数に[CmdletBinding()]を付けてください。それだけで呼び出し側が-Verboseを指定したときだけ表示されるようになります。恒常的に出したい場合は$VerbosePreference = 'Continue'をスクリプト冒頭で設定します。同様に、Write-Debugは-Debug、Write-Warningは-WarningActionで呼び出し側から制御できます。自前のログレベル変数を作るより、PowerShellの標準の仕組みに乗せたほうが、他人が読んだときに意図が伝わります。
詳細ログや警告も含めて、まるごとファイルに残すにはどうすればよいですか?
用途によって3通りあります。単純に全ストリームをファイルに落とすなら*> でリダイレクトします。実行の証跡として画面に出たものを丸ごと残すならStart-Transcriptが手軽です。プログラムから後で解析したいなら、1行1JSONの構造化ログを自分のログ関数で書き出すのが確実で、この場合でも保険としてトランスクリプトを併用する価値があります。
Write-Progressの表示は、ログファイルに残せますか?
残せません。進捗表示はホストの表示機能であり、リダイレクト可能なデータストリームとは別の扱いだからです。無人実行では表示先がないため、進捗はログに残す情報とは分けて考えてください。無人実行では$ProgressPreference = 'SilentlyContinue'にして進捗表示自体を止めると、環境によっては目に見えて速くなることもあります。進捗をログに残したいなら、Write-Verboseで「100件中50件完了」といった節目だけを書くほうが実用的です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る