PowerShell 7.3の試験的な機能「Cleanブロック」について

2022.04.30

しばたです。

PowerShell最新のプレビューバージョンであるPowerShell 7.3-preview.1から試験的な機能に「Cleanブロック」が追加されました。
この機能は破壊的変更となる大きな変更であるため本記事で解説したいと思います。

Cleanブロックとは?

はじめにこのCleanブロックがどういったものかと導入に至る動機について解説します。

従来PowerShellの高度な関数においてオブジェクトパイプラインを処理するため、beginprocessendという3つのブロックが提供されていました。
各ブロックはそれぞれ以下の役割を持ちます。

  • beginブロック : 関数の始まりに一度だけ呼び出されるブロック
  • processブロック : 関数にパイプラインからオブジェクトが渡される都度呼び出されるブロック
  • endブロック : 関数の終了時に一度だけ呼び出されるブロック

たとえば以下の様なWrite-SomeValueという関数を定義した場合、

function Write-SomeValue {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [string]$InputValue
    )
    begin {
        Write-Host -ForegroundColor Green "Begin : $InputValue"
    }
    process {
        Write-Host -ForegroundColor Gree "Process : $InputValue"
        Write-Output $InputValue
    }
    end {
        Write-Host -ForegroundColor Gree "End : $InputValue"
    }
}

この関数をパイプラインから呼び出すと以下の様になります。

# パイプラインから Write-SomeValue を呼び出す
1,2,3 | Write-SomeValue

パイプラインからint型の1,2,3がそれぞれWrite-SomeValueに渡され、最初に1回beginブロックが、次に3回processブロック、最後に1回endブロックが呼び出される形となります。

ただ、endブロックに関しては、例えばprocessブロック内で例外が発生した場合など特定の条件において呼び出されないことがあります。
一番再現しやすい例としてはSelect-Object-Firstパラメーターを使い出力オブジェクトを絞った場合が挙げられます。

# Select-Object -First パラメーターを使った場合endブロックが呼び出されない
1,2,3 | Write-SomeValue | Select-Object -First 2

# ちなみにendブロックを呼び出したい場合は-Waitパラメーターも一緒に指定する必要がある...が、これはこれで挙動が直観的ではないので注意
1,2,3 | Write-SomeValue | Select-Object -First 2 -Wait

この「endブロックが呼び出されない場合がある」挙動は関数内で適切な解放処理が必要となるリソース(例えばファイルなど)を扱う際に問題となり、昔から多くの人が苦労しています。
先人の苦労については以下の記事が詳しいのでご覧ください。

この問題を解消するために専用の構文が必要という流れになり、2021年にRFCの形で提案、最終的に新たにリソース解放用の新しい構文となる「cleanブロック」が追加されることになりました。

Cleanブロック詳細

Cleanブロックの仕様について簡単に説明すると以下の様になります。

  • cleanブロックは他の名前付きブロック(begin,process,end)が一度でも実行された場合に実行される
  • cleanブロックのスコープは他の名前付きブロックと同じである
  • パイプラインで例外が発生しない場合はendブロックの後に一度だけ実行される
  • パイプラインで例外が発生した場合はcleanブロックが実行された後に例外が伝播される
  • cleanブロックでは標準ストリーム(>1)への出力は破棄される
    • -OutVariableパラメーターでも出力をキャプチャすることはできない
    • その他のストリームへの出力はサポートされる
  • cleanブロックで例外が発生した場合はエラーオブジェクトがエラーストリーム(>2)へ出力され、例外の伝播はしない

リソース解放を目的としているため標準ストリームへの出力が破棄される点が特徴的ですね。
その他の細かい仕様については前述のRFCを参照してください。

試してみた

ここからは実際に機能を試してみます。
今回は私の開発環境であるWindows 10にインストール済みのPowerShell 7.3-preview.3を使います。

この機能は「PSCleanBlock」という名前で提供されてるのでGet-ExperimentalFeatureコマンドで確認してみます。

# 試験的な機能のチェック
Get-ExperimentalFeature -Name PSCleanBlock | Format-List

デフォルト無効となっているのでEnable-ExperimentalFeatureコマンドを使い有効にしてコンソールを再起動します。

# PSCleanBlockの試験的な機能を有効に (再起動後から有効)
Enable-ExperimentalFeature -Name PSCleanBlock

これでcleanブロックが利用可能になります。
まずは、最初の例Write-SomeValueにcleanブロックを追加してみます。

# Write-SomeValue に cleanブロック を追加
function Write-SomeValue {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [string]$InputValue
    )
    begin {
        Write-Host -ForegroundColor Green "Begin : $InputValue"
    }
    process {
        Write-Host -ForegroundColor Gree "Process : $InputValue"
        Write-Output $InputValue
    }
    end {
        Write-Host -ForegroundColor Gree "End : $InputValue"
    }
    # cleanブロックを追加
    clean {
        Write-Host -ForegroundColor Yellow "Clean! : $InputValue"
        Write-Output "Clean! : $InputVal"
    }
}

# パイプラインから Write-SomeValue を呼び出す
1,2,3 | Write-SomeValue

# Select-Object -First パラメーターを使った場合でもcleanブロックは呼び出される
1,2,3 | Write-SomeValue | Select-Object -First 2

関数を実行した結果は下図のとおり、常にcleanブロックが呼び出される様になりました。

もう少し実践的例としてRFCにあるPingのサンプルも試してみます。
こちらの例の方がcleanブロックの利用用途が分かりやすいと思います。

# RFCにあるPingのサンプル
using namespace System.Net.NetworkInformation

# 分かりやすさのために Write-Verbose を追加で仕込み
function Get-PingReply {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('IPAddress', 'Destination')]
        [string] $Target
    )
    begin {
        Write-Verbose "begin block called!"
        $Ping = [Ping]::new()

        # Timeout is in ms
        $Timeout = 2500
        [byte[]] $Buffer = 1..32

        # 128 TTL and avoid fragmentation
        $PingOptions = [PingOptions]::new(128, $true)
    }
    process {
        Write-Verbose "process block called! (target:$Target)"
        $Ping.Send($Target, $Timeout, $Buffer, $PingOptions)
    }
    clean {
        Write-Verbose "clean block called!"
        if ($Ping) { $Ping.Dispose() }
    }
}

こちらを実行した結果はこんな感じです。

# -Verbose を付けて実行
'1.2.3.4', 'www.google.com', '8.8.8.8' | Get-PingReply -Verbose

図だとちょっとわかりにくいですが、この関数を実行中にctrl+cを押して処理をキャンセルした場合もちゃんとcleanブロックは実行されます。

注意事項

最後に注意事項を何点か。

まずこのcleanブロックはまだ試験的な機能であり正式に導入されるタイミングは決まっていません。
また仕様についてもRFCをベースにしているので今から大きく変わることは無いと思われますが、RFC自体が改版される可能性はまだあります。

そしてcleanというキーワードは元々PowerShellで予約されていたわけではないため、cleanという名前の関数を既に使っている場合は意図しない挙動になるでしょう。
このためこの試験的な機能の導入は破壊的変更となります。

終わりに

以上となります。

PowerShell使いにとっては長く待ちわびた機能が導入され嬉しい限りです。
いつになるかはわかりませんが早く正式な機能になって欲しいですね。