🛫

PowerShell特有の落とし穴!Function内で標準出力すると戻り値が System.Object[] になってしまう仕様の解説

に公開1

PowerShellでは「Function内の標準出力が戻り値として呼び出し元に渡される」という仕様があります。

要は『 Function内で標準出力すると意図しない戻り値になるよ 』ということ。

何を隠そう私も時間が経つと、この仕様を忘れてしまい何度かハマっています……
私自身の備忘もかねて、この仕様の詳しい情報と対処方法を紹介。

この記事のターゲット

  • 主にPowerShell初学者・初級者の方
  • 下記のようなケースの原因不明なエラーでお悩みの方
    • 複数のFunctionで連携するPowerShellスクリプトを実行するとエラーが発生
    • コードを見直すが問題がないように見え、なぜエラーが発生しているか不明
    • エラーメッセージをみると、なぜかFunctionの戻り値が想定外のデータ型で返っている

「Function内で標準出力すると意図しない戻り値になる」とは

言葉の通りなのですが文章だけではイメージが掴みにくいと思うので、その事象となるコードを記載します。

仕様で戻り値が期待値とならないコード

意図しない戻り値にさせてしまっているポイントには「❌マーク」でコメントしています。

仕様で戻り値が期待値とならないコードの例
# Function
function New-Folder {
    param (
        [string]$FolderPath
    )
    # 処理結果の進捗を入れる数値型のコード
    [int]$statusCode = 0

    if (Test-Path -Path $FolderPath) {
        # すでにフォルダーがある場合は削除
        try {
            Remove-Item -Path $FolderPath -Recurse -Force
        }
        catch {
            Write-Error "New-Folder: フォルダー削除でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -101
        }

        # 削除後にフォルダーを作成
        if ($statusCode -eq 0) {
            try {
                # ❌ New-Itemのコマンド結果が戻り値に入る
                New-Item -ItemType Directory -Path $FolderPath
                # ❌ Write-Outputのコマンド結果が戻り値に入る
                Write-Output "New-Folder: フォルダーを再作成。"
            }
            catch {
                Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
                $statusCode = -201
            }
        }
    }
    else {
        # フォルダーがない場合はそのまま作成
        try {
            # ❌ New-Itemのコマンド結果が戻り値に入る
            New-Item -ItemType Directory -Path $FolderPath
            # ❌ Write-Outputのコマンド結果が戻り値に入る
            Write-Output "New-Folder: フォルダーを新規作成。"
        }
        catch {
            Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -301
        }
    }

    # ❌ Write-Outputのコマンド結果が戻り値に入る
    if ($statusCode -eq 0) {
        Write-Output "New-Folderが正常終了。statusCode[$($statusCode)]"
    }
    else {
        Write-Output "New-Folderが異常終了。statusCode[$($statusCode)]"
    }

    # ❌ Intで返ってほしいが、標準出力の文字列型も入ってObject型として返してしまう。
    return $statusCode
}

# メイン処理
[int]$statusCode = 0

# ❌ Function実行している場所で実行するとObject型で返ってくる。
#     戻り値をInt型の$statusCodeに代入しているのでデータ型の変換処理が発生。Object型からInt型には変換不可となるためココでエラーが発生。
#     $stautsCode に値を代入するタイミングでエラーが発生しているため、$statusCodeは「0」のままとなる。
$statusCode = (New-Folder "D:\Temp")

# ❌ 上記でエラーが発生しているものの$statusCodeは「0」のままなので正常終了のメッセージが出力
if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}

このようなコードを実行すると Function(New-Folder)の戻り値は、数値型(Int)ではなく Function内で出力された標準出力結果(Write-OutputNew-Item)を含む オブジェクト型(System.Object[])が戻り値に。

つぎは実際にこのコードを実行してみます。

“仕様で戻り値が期待値とならないコード”を実行した結果

“仕様で戻り値が期待値とならないコード”を実行すると下記のようなエラーが発生します。

“仕様で戻り値が期待値とならないコード”を実行した結果
MetadataError:
Line |
  63 |  $statusCode = (New-Folder "D:\Temp")
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32".
メイン処理が正常終了。statusCode[0]

日本語の翻訳すると下記のとおり。

実行した結果を日本語に翻訳
メタデータエラー:
行 |
  63 |  $statusCode = (New-Folder "D:\Temp")
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | "System.Object[]" 型の値 "System.Object[]""System.Int32" 型に変換することができません。
メイン処理が正常終了。statusCode[0]

前述している通り Function(New-Folder)を実行した戻り値が、System.Int32 ではなく 「System.Object[]」で返ってきたことがわかります。
では、実際にどのような流れで「System.Object[]」が返ってきたのかデバッグして詳細を確認してみましょう。

調査のためデバッグメッセージを追加したコード

Functionを実行する前後の $statusCode のデータ型と中身を確認します。
確認する方法として、Write-Debug を使いデバッグモード中にメッセージを出力するように修正。

PowerShellでデバッグモードをオンにするには、事前に下記を設定しておく必要があります。

デバッグモードをオンにする設定
$DebugPreference = "Continue"

ちなみにデバッグモードから抜ける場合は下記を設定します。

デバッグモードをオフにする設定(切り戻し用)
$DebugPreference = "SilentlyContinue"

デバッグメッセージは下記のように追加します。

デバッグメッセージを追加
# メイン処理
[int]$statusCode = 0

+Write-Debug ($statusCode.GetType().FullName)
+Write-Debug "statusCode[$($statusCode)]"
$statusCode = (New-Folder "D:\Temp")
+Write-Debug ($statusCode.GetType().FullName)
+Write-Debug "statusCode[$($statusCode)]"

if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}
調査のためデバッグメッセージを追加したコード(全体)

デバッグ関連を追加したコード。
追加した箇所には「📑マーク」を入れています。

調査のためデバッグメッセージを追加したコード(全体)
# 📑デバッグモードをオン
$DebugPreference = "Continue"
# $DebugPreference = "SilentlyContinue"

# Function群
function New-Folder {
    param (
        [string]$FolderPath
    )
    # 処理結果の進捗を入れる数値型のコード
    [int]$statusCode = 0

    if (Test-Path -Path $FolderPath) {
        # すでにフォルダーがある場合は削除
        try {
            Remove-Item -Path $FolderPath -Recurse -Force
        }
        catch {
            Write-Error "New-Folder: フォルダー削除でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -101
        }

        # 削除後にフォルダーを作成
        if ($statusCode -eq 0) {
            try {
                # ❌ New-Itemのコマンド結果が戻り値に入る
                New-Item -ItemType Directory -Path $FolderPath
                # ❌ Write-Outputのコマンド結果が戻り値に入る
                Write-Output "New-Folder: フォルダーを再作成。"
            }
            catch {
                Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
                $statusCode = -201
            }
        }
    }
    else {
        # フォルダーがない場合はそのまま作成
        try {
            # ❌ New-Itemのコマンド結果が戻り値に入る
            New-Item -ItemType Directory -Path $FolderPath
            # ❌ Write-Outputのコマンド結果が戻り値に入る
            Write-Output "New-Folder: フォルダーを新規作成。"
        }
        catch {
            Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -301
        }
    }

    # ❌ Write-Outputのコマンド結果が戻り値に入る
    if ($statusCode -eq 0) {
        Write-Output "New-Folderが正常終了。statusCode[$($statusCode)]"
    }
    else {
        Write-Output "New-Folderが異常終了。statusCode[$($statusCode)]"
    }

    # ❌ Intで返ったほしいが、標準出力の文字列型も入ってObject型として返してしまう。
    return $statusCode
}

# メイン処理
[int]$statusCode = 0

# 📑デバッグメッセージ追加
Write-Debug ($statusCode.GetType().FullName)
Write-Debug "statusCode[$($statusCode)]"
# ❌ Object型で戻ったことによりInt型に変換使用としてエラーが発生。$statusCodeは「0」のままとなる。
$statusCode = (New-Folder "D:\Temp")
# 📑デバッグメッセージ追加
Write-Debug ($statusCode.GetType().FullName)
Write-Debug "statusCode[$($statusCode)]"

# ❌ 上記でエラーが発生しているものの$statusCodeは「0」のままなので正常終了のメッセージが出力
if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}

“調査のためデバッグメッセージを追加したコード”を実行した結果

“調査のためデバッグメッセージを追加したコード”を実行した結果
DEBUG: System.Int32
DEBUG: statusCode[0]
MetadataError:
Line |
  64 |  $statusCode = (New-Folder "D:\Temp")
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32".
DEBUG: System.Int32
DEBUG: statusCode[0]
メイン処理が正常終了。statusCode[0]
PS C:\Users\XXXX>

Function(New-Folder)を実行する前後で $statusCode の結果を確認すると、データ型は System.Int32 で 中身は数字 0 のままです。

つぎに Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32". というメッセージを見ていきます。
Function(New-Folder)の戻り値は、PowerShell特有の仕様により System.Object[] で返り、そのデータを数値型の $statusCode に代入しようとしているため、エラーが発生。
このエラーは、意図しないデータ型の戻り値だったから発生したのではなく、データ型の変換処理が行われた結果、発生したエラーです。
これもPowerShellの仕様なのですが、別のデータ型の変数に代入すると自動でキャスト(型の変換)が発生。
そのキャスト処理が実行されたものの、System.Object[] から System.Int32 はキャスト(変換)できないデータ型同士なので Cannot convert から始まる データ型の変換でエラーが発生した という流れとなります。

あらためて箇条書きで処理の流れを説明すると……

本来、Function(New-Folder)で期待していたデータ型は、「System.Int32」。
しかし、Function内の標準出力により「@("New-Itemの標準出力結果", "Write-Outputの標準出力結果", "$statusCodeの数値")」というような System.Object[] の戻り値として返ってくる。
その意図しないオブジェクト型(System.Object[])の戻り値を異なる数値型(System.Int32)に代入されたときにキャスト処理が発生し、データ型の変換が不可能なため データ型の変換エラーが発生

となります。

では、実際に戻ってきた System.Object[] の中身が予想通りか見てみましょう。

本当に戻り値が System.Object[] で標準出力も含まれているか確認

実際にFunction(New-Folder)の戻り値内を確認してみます。
確認するために新たにコードを追加!

戻り値のオブジェクト型の中身をチェックするコード

戻り値のオブジェクト型の中身をチェックするコードを追加
# メイン処理
-[int]$statusCode = 0
+[object]$resultData = @()

-$statusCode = (New-Folder "D:\Temp")
+$resultData = (New-Folder "D:\Temp")
+$resultData | Format-List

if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}
戻り値のオブジェクト型の中身をチェックするコード(全体)

上記のとおり、変数($resultData)の中身を確認するためのコードを追加。
変化点には「👀マーク」を入れてコメントしてます。

戻り値のオブジェクト型の中身をチェックするコード(全体)
# Function群
function New-Folder {
    param (
        [string]$FolderPath
    )
    # 処理結果の進捗を入れる数値型のコード
    [int]$statusCode = 0

    if (Test-Path -Path $FolderPath) {
        # すでにフォルダーがある場合は削除
        try {
            Remove-Item -Path $FolderPath -Recurse -Force
        }
        catch {
            Write-Error "New-Folder: フォルダー削除でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -101
        }

        # 削除後にフォルダーを作成
        if ($statusCode -eq 0) {
            try {
                New-Item -ItemType Directory -Path $FolderPath
                Write-Output "New-Folder: フォルダーを再作成。"
            }
            catch {
                Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
                $statusCode = -201
            }
        }
    }
    else {
        # フォルダーがない場合はそのまま作成
        try {
            New-Item -ItemType Directory -Path $FolderPath
            Write-Output "New-Folder: フォルダーを新規作成。"
        }
        catch {
            Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -301
        }
    }
    
    if ($statusCode -eq 0) {
        Write-Output "New-Folderが正常終了。statusCode[$($statusCode)]"
    }
    else {
        Write-Output "New-Folderが異常終了。statusCode[$($statusCode)]"
    }
    
    return $statusCode
}

# メイン処理
# 👀 オブジェクト型として受け取るため変数を宣言
[object]$resultData = @()

# 👀 オブジェクト型として受け取る
$resultData = (New-Folder "D:\Temp")

# 👀 オブジェクト型の中身を確認
$resultData | Format-List

if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}

“戻り値のオブジェクト型の中身をチェックするコード”を実行した結果

下記が実行した結果です。

“戻り値のオブジェクト型の中身をチェックするコード”を実行した結果


    Directory: D:\

Name           : Temp
CreationTime   : 2025/03/13 12:28:42
LastWriteTime  : 2025/03/13 12:28:42
LastAccessTime : 2025/03/13 12:28:42
Mode           : d----
LinkType       :
Target         :

New-Folder: フォルダーを新規作成。
New-Folderが正常終了。statusCode[0]
0
メイン処理が正常終了。statusCode[0]
PS C:\Users\XXXX>

実行結果の一つひとつを分類してみると……

下記は「New-Itemコマンドレットの標準出力」の結果。

New-Itemコマンドレットの標準出力

    Directory: D:\

Name           : Temp
CreationTime   : 2025/03/13 12:28:42
LastWriteTime  : 2025/03/13 12:28:42
LastAccessTime : 2025/03/13 12:28:42
Mode           : d----
LinkType       :
Target         :

つぎは「Write-Outputの標準出力」の結果。

Write-Outputの標準出力
New-Folder: フォルダーを新規作成。
New-Folderが正常終了。statusCode[0]

その次は「Functionでreturnした数値型の戻り値」の結果。

Functionでreturnした数値型の戻り値
0

さいごは「メイン処理側、Write-Outputの標準出力」の結果となります。

メイン処理が正常終了。statusCode[0]

全体の流れを時系列で説明

すこし冗長な表現になってしまいますが、しっかりと伝える事を目的に説明の仕方を変えて、
時系列形式で処理の流れを記載します。

  • メイン処理(Functionを呼び出す前)

    1. あらかじめステータスコードに「0」をセット
    2. Function(New-Folder)を呼び出し
  • Function処理

    1. あらかじめステータスコードに「0」をセット
    2. 対象のフォルダーを作成
      このタイミングで標準出力あり
      New-Itemコマンドレットにより標準出力され、明示的に指定が無くても戻り値として返されてしまう。
    3. Functionの処理結果を出力
      このタイミングで標準出力あり
      Write-Outputコマンドレットにより標準出力され、明示的に指定が無くても戻り値として返されてしまう。
    4. ステータスコードを戻り値に返す
      ココで戻り値がオブジェクト型に
      これまでの処理で標準出力された結果が自動的に戻り値として返される。
      戻り値のオブジェクト型の中身は、@("New-Itemの結果", "Write-Outputの結果", "ステータスコードの数値") となる。
  • メイン処理(Functionを呼び出した後)

    1. Functionの結果をステータスコードに代入
      ココでエラーが発生
      Functionの戻り値が オブジェクト型 であり、数値型 のステータスコードに代入しようと自動でデータ型の変換を行うとするが、
      オブジェクト型 から 数値型 への変換は不可のため、変換エラーが発生。
       
      なお、この代入時のエラーによりステータスコードは「0」のままで次へ
    2. メイン処理の終了メッセージ
      ちなみにステータスコードは「0」のままなので正常終了として完了する。

解決方法

解決方法は簡単で「Function内では、標準出力しないように変更して想定通りの戻り値にする」だけです。

これまで題材にしたコードに対して 「標準出力しないように変更」してみます。

標準出力しないように変更したコード

New-Itemなどの標準出力を伴うコマンドレットは、コマンド > $null を使って標準出力を破棄します。

-New-Item -ItemType Directory -Path $FolderPath
+(New-Item -ItemType Directory -Path $FolderPath > $null)

Write-Output ではなく Write-Host により 標準出力 から コンソール出力 に変更。
※ コンソール出力とは、リダイレクトやパイプ処理が可能な標準出力はなく画面のみに出力する方式のこと。そのため、Function内で実行しても戻り値に影響しない。

-Write-Output "New-Folder: フォルダーを再作成。"
+Write-Host "New-Folder: フォルダーを再作成。"
標準出力しないように変更したコード(全体)

変化点を「✅マーク」でコメントしています。

標準出力しないように変更したコード
# Function群
function New-Folder {
    param (
        [string]$FolderPath
    )
    # 処理結果の進捗を入れる数値型のコード
    [int]$statusCode = 0

    if (Test-Path -Path $FolderPath) {
        # すでにフォルダーがある場合は削除
        try {
            Remove-Item -Path $FolderPath -Recurse -Force
        }
        catch {
            Write-Error "New-Folder: フォルダー削除でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -101
        }

        # 削除後にフォルダーを作成
        if ($statusCode -eq 0) {
            try {
                # ✅ New-Item 実行時の標準出力を > $null で破棄
                (New-Item -ItemType Directory -Path $FolderPath > $null)
                # ✅ Write-Output から Write-Host による変更で 標準出力 から コンソール(画面のみ) に!
                Write-Host "New-Folder: フォルダーを再作成。"
            }
            catch {
                Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
                $statusCode = -201
            }
        }
    }
    else {
        # フォルダーがない場合はそのまま作成
        try {
            # ✅ New-Item 実行時の標準出力を > $null で破棄
            (New-Item -ItemType Directory -Path $FolderPath > $null)
            # ✅ Write-Output から Write-Host による変更で 標準出力 から コンソール(画面のみ) に!
            Write-Host "New-Folder: フォルダーを新規作成。"
        }
        catch {
            Write-Error "New-Folder: フォルダー作成でエラーが発生。FolderPath[$($FolderPath)]エラー[$($_.Exception.Message)]"
            $statusCode = -301
        }
    }

    # ✅ Write-Output から Write-Host による変更で 標準出力 から コンソール(画面のみ) に!
    if ($statusCode -eq 0) {
        Write-Host "New-Folderが正常終了。statusCode[$($statusCode)]"
    }
    else {
        Write-Host "New-Folderが異常終了。statusCode[$($statusCode)]"
    }

    # ✅ Function内で標準出力がないのでIntで返る
    return $statusCode
}

# メイン処理
[int]$statusCode = 0

# ✅ Functionの結果がIntで返り、変換することなくステータスコードに結果が代入
$statusCode = (New-Folder "D:\Temp")

# ✅ 実際の結果で分岐される
if ($statusCode -eq 0) {
    Write-Host "メイン処理が正常終了。statusCode[$($statusCode)]"
}
else {
    Write-Host "メイン処理が異常終了。statusCode[$($statusCode)]"
}

“標準出力しないように変更したコード”を実行した結果

下記のように問題なく正常終了しました。

“標準出力しないように変更したコード”を実行した結果
New-Folder: フォルダーを再作成。
New-Folderが正常終了。statusCode[0]
メイン処理が正常終了。statusCode[0]

まとめ

最後にかみ砕いて、わかりやすいコードを例にまとめてみます。
また、「標準出力しないように変更」以外に考えうる対応方法も紹介。

事例「Function内で標準出力すると意図しない戻り値」

Function内で Write-Output のように標準出力を伴うコマンドレットを使用することで、意図せずその結果が戻り値として返される。

コード例

function Get-Thirteen {
    Write-Output "Get-Thirteen 処理開始"    # ❌ この標準出力も戻り値として返る
    return 13
}

$result = Get-Thirteen
Write-Host "結果: $result"                  # "Get-Number 処理開始" と 13 の両方が出力される

対応方法の一覧

  1. 標準出力が不要である場合、変更または削除する(前述で説明済みの内容)
    標準出力が必須ではないのであれば、Write-Host などコンソール出力するコマンドレットに変更する。本当にいらない場合は Write-Output そのものを削除。
    New-Item をはじめとするコマンドレットの機能そのものは実行したいが標準出力は不要という場合は、下記のようにコマンドレットの標準出力を破棄する。

    • コマンド > $null 👈 Windows以外でも使用されていてパフォーマンスも速いらしい。
    • コマンド | Out-Null 👈 PowerShellのコマンドレットで全体的に統一感がでて可読性が良い。
    • [void](コマンド) 👈 可読性が悪く一般的にわかりにくい記述の仕方。
    • $null = コマンド 👈 同上。
    コード例:標準出力が不要である場合、変更または削除する
    function Get-Thirteen {
    -    Write-Output "Get-Thirteen 処理開始"   # ✅ 不要な標準出力を削除する
        return 13
    }
    
    $result = Get-Thirteen
    Write-Host "結果: $result"                  # 期待値の 13 が出力される
    
    function Get-Thirteen {
    -    Write-Output "Get-Thirteen 処理開始"
    +    Write-Host "Get-Thirteen 処理開始"      # ✅ コンソール出力に変更
        return 13
    }
    
    $result = Get-Thirteen
    Write-Host "結果: $result"                  # 期待値の 13 が出力される
    
    function Get-Thirteen {
    -    Write-Output "Get-Thirteen 処理開始"
    +    Write-Output "Get-Thirteen 処理開始" > $null  # ✅ 標準出力を破棄する(Write-Outputの標準出力を破棄するのは変なコードですが……)
        return 13
    }
    
    $result = Get-Thirteen
    Write-Host "結果: $result"                  # 期待値の 13 が出力される
    
  2. デバッグメッセージの場合はデバッグ用として設定する
    調査フェーズのコード紹介で前述したとおり、デバッグメッセージを残す意図がある場合は Write-Debug を活用。
    実行時に「-Debug」というパラメーターを使用する方法でも簡単にデバッグモードで実行可能です。

    コード例:デバッグメッセージの場合はデバッグ用として設定する
    function Get-Thirteen {
    +    [CmdletBinding()]                        # ✅ Debugオプションを受付可能に
    +    param ()                                 # ✅ 同上
    -    Write-Output "Get-Thirteen 処理開始"
    +    Write-Debug "Get-Thirteen 処理開始"      # ✅ デバッグ出力に変更
        return 13
    }
    
    -$result = Get-Thirteen
    +$result = (Get-Thirteen -Debug)              # ✅ デバッグモードでFunctionを実行
    Write-Host "結果: $result"                   # 期待値の 13 が出力される(デバッグ出力あり)
    
    function Get-Thirteen {
    -    Write-Output "Get-Thirteen 処理開始"
    +    Write-Debug "Get-Thirteen 処理開始"      # ✅ デバッグ出力に変更
        return 13
    }
    
    +$DebugPreference = "Continue"                # ✅ デバッグモードにする
    $result = Get-Thirteen
    Write-Host "結果: $result"                   # 期待値の 13 が出力される(デバッグ出力あり)
    
  3. Function内の結果をどうしても標準出力したい場合は、戻り値を配列に変えよう
    スクリプトの設計によってはFunction内で得た情報をどうしても標準出力したい場合があると思います。
    そのような場合は、あらかじめ設定する戻り値を配列にする方法がオススメです。
    配列におけるエラー制御の方法ですが、最初に配列を初期化しておき配列にデータがあれば正常終了とし、
    データがない場合(@()などで判定)は異常終了するという動きが良いのでしょう。

    コード例:Function内の結果をどうしても標準出力したい場合は、戻り値を配列に変えよう
    function Get-Thirteen {
    +    [object[]]$objectArray = @()                               # ✅ 戻り値にする変数を宣言
    -    Write-Output "Get-Thirteen 処理開始"
    +    $objectArray += (Write-Output "Get-Thirteen 処理開始")     # ✅ 標準出力を配列に追加
    +    $objectArray += 13                                         # ✅ 標準出力を配列に追加
    -    return 13
    +    return $objectArray                                        # ✅ 戻り値を配列にして返す
    }
    
    $result = Get-Thirteen
    -Write-Host "結果: $result"
    +Write-Output "結果: $result"                                   # ✅ Functionの結果を標準出力。"Get-Number 処理開始" と 13 の両方が返される
    

これでこの記事は終わりです。

今回、冒頭の解説で使用したコード例では戻り値を代入する $statusCode にデータ型の「IntSystem.Int32)」を設定していました。
仮にデータ型を設定せずに実行すると変換エラーが起きずにそのまま処理が進むことになるでしょう。
そうなると想定外のことが起きているのに気付けず、その後の処理でエラーや期待する動作をしなかった場合、原因の切り分けが難しくなります。

簡単にコーディングできる事も売りであるスクリプト言語のPowerShellですが、ある程度の規模で書く場合は「変数にデータ型を設定する」というのが、
他のプログラム言語と同様、重要なポイントになると思います。

この変な仕様、理由があってのことだと思いますが、仮に私が「PowerShellで嫌なことは?」と聞かれた場合は、真っ先にこのPowerShell独自の仕様と答えるでしょう。

参考文献

https://zenn.dev/haretokidoki/articles/24ac3ba42d8050

https://stackoverflow.com/questions/5260125/whats-the-better-cleaner-way-to-ignore-output-in-powershell?form=MG0AV3&form=MG0AV3

関連記事

https://haretokidoki-blog.com/pasocon_powershell-startup/
https://zenn.dev/haretokidoki/articles/7e6924ff0cc960
https://zenn.dev/haretokidoki/articles/fb6830f9155de5

GitHubで編集を提案

Discussion

akiGAMEBOY५✍🤖はれときどきZennakiGAMEBOY५✍🤖はれときどきZenn

加筆予定のものをメモ。

Read-Hostを含むFunction でもSystem.Object[] になる場合あり!

コード例

例として下記のようなFunctionを作ってみました。
メモリ使用率を表示するFunctionで結果をWrite-Hostで表示した後にRead-Hostで処理を一時的に止めています。

Read-Hostを含むFunction
# Functionの定義
Function Get-MemoryUsage {
    $statusCode = 0
    
    $memoryInfo = @{}
    
    try {
    	# システム情報を取得
        $systemInfo = Get-CimInstance Win32_OperatingSystem
        
        # メモリ使用率の計算
        $totalMem = $systemInfo.TotalVisibleMemorySize
        $freeMem = $systemInfo.FreePhysicalMemory
        $usedMem = $totalMem - $freeMem
        $usagePercentage = ($usedMem / $totalMem) * 100
        
        # ハッシュテーブル化
        $memoryInfo = [PSCustomObject]@{
        	TotalMemoryMB  = [math]::Round($totalMemory / 1024, 2)
        	UsedMemoryMB   = [math]::Round($usedMemory / 1024, 2)
        	FreeMemoryMB   = [math]::Round($freeMemory / 1024, 2)
        	UsagePercentage = [math]::Round($usagePercentage, 2)
    	}
    }
    catch {
        Write-Error "例外エラーが発生: $($_.Exception.Message)"
        $statusCode = -1
    }
    
    # 結果を表示
    Write-Host $memoryInfo.UsagePercentage
    Read-Host 'Enterキーで処理を再開してください。'
    
    return $statusCode
}

実際に実行した結果

下記のとおり実行後にデータ型や値を確認するコードを作成。

実行する際のコード
# Funcitonの実行
$returnData = Get-MemoryUsage

# 中身を確認
Write-Output "returnData - Datatype:「$($returnData.GetType().FullName)」"
Write-Output "returnData - Length  :「$($returnData.Length)」"
Write-Output "returnData           :「$returnData」"
Write-Output "returnData[0] - Value:「$($returnData[0])」"
Write-Output "resultData[1] - Value:「$($returnData[1])」"

実行してみると……

中身を確認した結果
PS C:\Users\XXXX> Write-Output "returnData - Datatype: $($returnData.GetType().FullName)"
>> Write-Output "returnData - Length: $($returnData.Length)"
>> Write-Output "returnData: $returnData"
>> Write-Output "returnData[0] - Value: $($returnData[0])"
>> Write-Output "resultData[1] - Value: $($returnData[1])"
returnData - Datatype: System.Object[]
returnData - Length: 2
returnData:  0
returnData[0] - Value:
resultData[1] - Value: 0
PS C:\Users\XXXX>

System.Object[]で返ってきてしまっていることがわかります。

原因「なぜRead-Hostでも System.Object[] になるのか」

答えは簡単、Read-HostでもWrite-Outputなどと同様に標準出力してしまう為です。

もっと細かくいうとRead-Hostで入力した値が自動で標準出力される仕様。

つまり、コード例で発生した流れは……

  1. Read-Hostで入力したEnterキー
  2. 空改行 = 空文字として認識
  3. 空文字が標準出力
  4. Function内の標準出力された空文字が自動で呼び出し元に戻る

となり期待する戻り値になりませんでした。

Read-Host とあるので Write-Host と同様に標準出力しないものと勘違いされやすいコマンドレットだと思います。

解決方法

これの解決方法は標準出力させなければよいので、Read-Host部分を

Read-Host部分の改善案1
# 結果を表示
Write-Host $memoryInfo.UsagePercentage
Read-Host 'Enterキーで処理を再開してください。' | Out-Null

のようにするとOK。

もし、Read-Host で入力した値を後続処理で使う場合は、

Read-Host部分の改善案2
# 結果を表示
Write-Host $memoryInfo.UsagePercentage
$userInput = Read-Host 'Enterキーで処理を再開してください。'
# 以降、$userInputを使う

のように変数するとよいでしょう。