PowerShell 7.6.4へ更新する前に、Join-Path、WildcardPattern.Escape()、PowerShell内部のトレースソース名を厳密に検証しているテストは見直しが必要です。最も影響を受けやすいのは、コマンドの実行可否ではなく、パラメーター型やエスケープ後の文字列、ソース名を完全一致で比較するテストです。
結論として、Join-Pathの-ChildPathはstring[]として扱い、単独バッククォートのエスケープ結果はバッククォート2個を期待し、GetHelpCommandの末尾空白は期待値から削除します。PowerShell 7.6.4は.NET 10.0.10上で動作し、Microsoftはこれら3点を公式に破壊的変更として案内しています。未解決の不具合ではないため、更新を避けるよりも、型前提とテスト期待値を新しい仕様へ合わせるのが基本対応です。(Microsoft Learn)
PowerShell 7.6.4で確認すべき3つの互換性変更
| 変更点 | PowerShell 7.6.4の動作 | 影響を受けやすいテスト | 修正方針 |
|---|---|---|---|
Join-Path -ChildPath | 型がSystem.String[]になり、複数の子パスを順番に連結できる | パラメーター型、バインドエラー、出力件数の検証 | 型期待値をstring[]へ変更し、出力は1つの連結済みパスとして検証 |
WildcardPattern.Escape() | 単独バッククォート自身もエスケープする | 戻り値の完全一致、-likeの結果、独自の二重エスケープ | バッククォート1個に対して2個を期待し、独自の追加エスケープを削除 |
GetHelpCommandソース名 | 末尾の空白が削除される | トレースソース名、スナップショット、許可リストの比較 | "GetHelpCommand "を"GetHelpCommand"へ修正 |
.NET 10.0.10はPowerShell 7.6.4の実行基盤を識別する情報です。ただし、今回の3つはPowerShell側のコマンドレットやエンジン実装の変更です。互換処理を分岐するときは、.NETのバージョンではなく$PSVersionTable.PSVersionを基準にする方が安全です。
更新前にPowerShellと.NETのバージョンを確認する
最初に、テスト対象が本当にPowerShell 7.6.4と.NET 10.0.10の組み合わせになっているかを確認します。
[pscustomobject]@{
PowerShell = $PSVersionTable.PSVersion.ToString()
PSEdition = $PSVersionTable.PSEdition
Runtime = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription
}
対象環境では、概ね次の内容が表示されます。
PowerShell : 7.6.4
PSEdition : Core
Runtime : .NET 10.0.10
CIや受け入れテストでは、次の2種類を分けて考えることが重要です。
| テストの種類 | 検証内容 |
|---|---|
| 配備バージョンテスト | PowerShell 7.6.4、.NET 10.0.10が配置されているか |
| 機能回帰テスト | Join-Pathやワイルドカード処理が期待する結果を返すか |
機能回帰テストまで.NETのパッチバージョンへ固定すると、将来.NETだけが更新された際に、機能とは無関係な失敗が発生します。バージョン固定が配備要件である場合だけ、実行環境の完全一致を別テストで確認してください。
Join-Pathの-ChildPathがstring[]になった影響
PowerShell 7.6では、Join-Pathの-ChildPathが単一文字列から文字列配列へ変更されました。複数の値を渡すと、それぞれが別の出力になるのではなく、順番に連結された1つのパスになります。
Join-Path -Path 'one' -ChildPath @('two', 'three')
Windowsでの結果は次のとおりです。
one\two\three
LinuxやmacOSでは、プロバイダーに応じて区切り文字がスラッシュになります。
公式ドキュメントでも、-ChildPathの型はString[]とされ、複数の子パスを一度に連結できることが明記されています。PowerShell本体のテストでも、配列を渡した結果の件数は1件として検証されています。(Microsoft Learn)
配列は「候補一覧」ではなく「パス階層」
次の指定は、logsとapp.logという2つのパスを返すものではありません。
$result = @(
Join-Path -Path 'root' -ChildPath @('logs', 'app.log')
)
$result.Count
$result[0]
結果は次のようになります。
1
root\logs\app.log
つまり、-ChildPath配列は次のように解釈されます。
root
└─ logs
└─ app.log
複数の独立したパスを作りたい場合は、明示的に繰り返します。
'logs', 'temp' | ForEach-Object {
Join-Path -Path 'root' -ChildPath $_
}
この場合は、次の2件が返ります。
root\logs
root\temp
型情報を検証するテストを修正する
旧仕様を前提とした次のテストは失敗します。
$parameterType = (Get-Command Join-Path).Parameters['ChildPath'].ParameterType
$parameterType | Should -Be ([string])
PowerShell 7.6.4向けには、期待値をstring[]へ変更します。
$parameterType = (Get-Command Join-Path).Parameters['ChildPath'].ParameterType
$parameterType | Should -Be ([string[]])
文字列名で比較している場合も修正が必要です。
$parameterType.FullName | Should -BeExactly 'System.String[]'
ラッパー関数の引数もstring[]へ変更する
Join-Pathを独自関数で包んでいる場合、ラッパー側が[string]のままだと、呼び出し元から渡された配列が意図しない文字列へ変換される可能性があります。
修正前の例です。
function Join-AppPath {
param(
[string] $Path,
[string] $ChildPath
)
Join-Path -Path $Path -ChildPath $ChildPath
}
PowerShell 7.6の機能を利用するなら、次のように変更します。
function Join-AppPath {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $Path,
[Parameter(Mandatory)]
[string[]] $ChildPath
)
Join-Path -Path $Path -ChildPath $ChildPath
}
呼び出し側では、複数の階層を自然に指定できます。
Join-AppPath `
-Path 'C:\Application' `
-ChildPath @('logs', '2026', 'app.log')
PowerShell 7.4などもサポートする場合
旧バージョンとPowerShell 7.6.4を同じコードでサポートする場合は、配列を直接-ChildPathへ渡さず、1階層ずつ連結する方法が確実です。
$segments = @('logs', '2026', 'app.log')
$result = 'C:\Application'
foreach ($segment in $segments) {
$result = Join-Path -Path $result -ChildPath $segment
}
$result
この方法なら、-ChildPathが単一文字列であるバージョンでも動作します。
一方、最低動作バージョンをPowerShell 7.6以上へ引き上げられる場合は、互換処理を残さず、配列を直接渡す方がコードは簡潔です。
AdditionalChildPathはすぐに削除しなくてもよい
従来の次の書き方も引き続き利用できます。
Join-Path one two three four
この場合、oneがPath、twoがChildPath、残りがAdditionalChildPathへ割り当てられます。PowerShell 7.6では、名前付きパラメーターで次のように書けるようになったと考えると分かりやすいでしょう。
Join-Path -Path one -ChildPath @('two', 'three', 'four')
公式ドキュメントでもAdditionalChildPathは残っており、ChildPath配列を代わりに利用できると説明されています。既存コードを一斉に書き換える必要はありません。(Microsoft Learn)
WildcardPattern.Escapeが単独バッククォートを正しく処理する
WildcardPattern.Escape()は、文字列を正規表現ではなく、PowerShellのワイルドカードパターンとして安全に扱うためのメソッドです。
PowerShellのワイルドカードではバッククォート自身がエスケープ文字です。そのため、入力文字列に含まれるバッククォートを文字どおりに比較するには、バッククォート自身もエスケープしなければなりません。
PowerShell 7.6.4では、次のように動作します。
$source = 'abc`def'
$escaped = [System.Management.Automation.WildcardPattern]::Escape($source)
$escaped
結果は次のとおりです。
abc``def
入力に含まれるバッククォートは1個ですが、エスケープ後は2個になります。
さらに、エスケープ後の文字列を-likeへ渡すと、元の文字列と正しく一致します。
$source -like $escaped
True
PowerShell本体には、入力がabc、バッククォート1個、defの組み合わせなら、戻り値ではバッククォート2個になることを検証するテストが追加されています。バッククォートが2個含まれる入力では、戻り値は4個になります。(GitHub)
完全一致テストの期待値を変更する
旧動作を前提とした次のテストは修正が必要です。
$result = [System.Management.Automation.WildcardPattern]::Escape('abc`def')
$result | Should -BeExactly 'abc`def'
PowerShell 7.6.4では、次の期待値に変更します。
$result = [System.Management.Automation.WildcardPattern]::Escape('abc`def')
$result | Should -BeExactly 'abc``def'
戻り値だけでなく、実際の一致結果も検証しておくと、単なる実装文字列の確認で終わりません。
$source = 'abc`def'
$pattern = [System.Management.Automation.WildcardPattern]::Escape($source)
$pattern | Should -BeExactly 'abc``def'
($source -like $pattern) | Should -BeTrue
独自のバッククォート置換を重ねない
以前の不具合を補うために、次のような処理を追加しているコードは注意が必要です。
$escaped = [System.Management.Automation.WildcardPattern]::Escape($source)
$escaped = $escaped.Replace('`', '``')
PowerShell 7.6.4では、Escape()自身がバッククォートを処理します。その後に再度置換すると、必要以上にエスケープされ、-likeで一致しなくなる可能性があります。
ユーザー入力をリテラルとしてワイルドカード比較する場合は、原則としてWildcardPattern.Escape()を1回だけ呼び出します。
$userInput = 'report`2026[final].txt'
$pattern = [System.Management.Automation.WildcardPattern]::Escape($userInput)
$fileName -like $pattern
Regex.Escapeとは用途が異なる
次の2つは置き換え可能ではありません。
[System.Management.Automation.WildcardPattern]::Escape($value)
[regex]::Escape($value)
WildcardPattern.Escape()は-likeやワイルドカード対応パラメーター向けです。
[regex]::Escape()は-matchや.NETの正規表現API向けです。テストを修正するときに、バッククォート処理を避ける目的でRegex.Escape()へ変更すると、比較文法そのものが変わってしまいます。
イベントソース名の変更はGetHelpCommandトレースソースが対象
公式の変更一覧では「event source name」と表現されていますが、実際に変更されたのは、PowerShell内部で使用されるGetHelpCommandのトレースソース名です。
変更前は末尾に空白が含まれていました。
GetHelpCommand
PowerShell 7.6.4では、末尾空白のない名前へ修正されています。
GetHelpCommand
PowerShell本体の変更では、TraceSource属性とPSTraceSource.GetTracer()へ渡す文字列の両方から末尾空白が削除されています。(GitHub)
Windowsイベントログ全般の変更ではない
この変更の影響範囲を誤って広げないことが重要です。
| 使用している仕組み | 今回の変更との関係 |
|---|---|
Get-TraceSource | 対象 |
Trace-CommandでPowerShell内部を追跡 | 対象になり得る |
PSTraceSource名のスナップショット比較 | 対象 |
Get-WinEventでWindowsイベントログを取得 | 直接の対象ではない |
Write-EventLogの独自イベントソース | 直接の対象ではない |
| ETWプロバイダー名全般 | この変更だけを根拠に一括変更しない |
Get-TraceSourceは、現在使用中のPowerShellコンポーネントに対応するトレースソースを返すコマンドレットです。GetHelpCommandを確実に読み込ませるため、先にGet-Helpを実行してから検証します。(Microsoft Learn)
$null = Get-Help Get-Help
$traceNames = @(
Get-TraceSource |
Select-Object -ExpandProperty Name
)
$traceNames -contains 'GetHelpCommand'
$traceNames -contains 'GetHelpCommand '
PowerShell 7.6.4では、期待する結果は次のとおりです。
True
False
末尾空白付きの期待値を修正する
修正前のテストです。
$traceNames | Should -Contain 'GetHelpCommand '
修正後は、末尾空白を削除します。
$traceNames | Should -Contain 'GetHelpCommand'
$traceNames | Should -Not -Contain 'GetHelpCommand '
設定ファイル、JSONスナップショット、許可リストに次の値を保存している場合も同様です。
{
"allowedTraceSources": [
"GetHelpCommand "
]
}
次のように修正します。
{
"allowedTraceSources": [
"GetHelpCommand"
]
}
複数バージョンを比較する場合は対象名だけ正規化する
PowerShell 7.4系と7.6系の結果を同じ基準で比較する必要がある場合は、既知の旧名称だけを正規化します。
function ConvertTo-CanonicalTraceSourceName {
param(
[Parameter(Mandatory)]
[string] $Name
)
if ($Name -eq 'GetHelpCommand ') {
return 'GetHelpCommand'
}
return $Name
}
すべてのイベントソース名へ無条件にTrim()を適用する方法は避けた方が安全です。本来保持すべき空白や、別のデータ不備まで見えなくなるためです。
PowerShell 7.6.4向けの最小Pester回帰テスト
次のテストを追加すれば、今回の3点をまとめて確認できます。
Describe 'PowerShell 7.6.4 compatibility checks' {
It 'uses string[] for Join-Path ChildPath' {
$parameterType =
(Get-Command Join-Path).Parameters['ChildPath'].ParameterType
$parameterType | Should -Be ([string[]])
$parameterType.FullName |
Should -BeExactly 'System.String[]'
}
It 'joins a ChildPath array into one path' {
$actual = @(
Join-Path `
-Path 'root' `
-ChildPath @('logs', 'app.log')
)
$expected = [System.IO.Path]::Combine(
'root',
'logs',
'app.log'
)
$actual.Count | Should -Be 1
$actual[0] | Should -BeExactly $expected
}
It 'escapes a lone backtick' {
$source = 'abc`def'
$escaped =
[System.Management.Automation.WildcardPattern]::Escape(
$source
)
$escaped | Should -BeExactly 'abc``def'
($source -like $escaped) | Should -BeTrue
}
It 'uses GetHelpCommand without a trailing space' {
$null = Get-Help Get-Help
$traceNames = @(
Get-TraceSource |
Select-Object -ExpandProperty Name
)
$traceNames | Should -Contain 'GetHelpCommand'
$traceNames | Should -Not -Contain 'GetHelpCommand '
}
}
[System.IO.Path]::Combine()を期待値の生成に使っているため、WindowsとLinuxでパス区切り文字が異なっても同じテストを利用できます。
更新前に検索すべきコード
対象ファイルを一つずつ確認するより、最初に関連コードを横断検索した方が効率的です。
$files = Get-ChildItem `
-Path . `
-Recurse `
-File `
-Include *.ps1, *.psm1, *.psd1
$files | Select-String `
-SimpleMatch `
-Pattern @(
'Join-Path',
'WildcardPattern',
'GetHelpCommand '
)
検索結果では、特に次の箇所を優先して確認します。
Join-Pathのパラメーター型を検証しているテスト-ChildPathへ配列を渡した際のエラーを期待しているテストJoin-Pathをラップする関数の[string] $ChildPathWildcardPattern.Escape()の戻り値を完全一致で比較するテストEscape()の後に独自のバッククォート置換をしている処理"GetHelpCommand "を含むスナップショットや許可リスト
PowerShell 7.6.4への移行手順
更新作業は、次の順序で進めると原因を切り分けやすくなります。
- 現行PowerShellで既存テストが成功することを確認する
- PowerShell 7.6.4と.NET 10.0.10の検証環境を用意する
- 関連コードを横断検索する
- 既存テストを変更せずに一度実行し、差分を記録する
ChildPathの型期待値をstring[]へ変更する- バッククォートのエスケープ期待値を修正する
GetHelpCommandの末尾空白を削除する- 独自の互換処理や二重エスケープが残っていないか確認する
- WindowsとLinuxを対象にする場合は、パス区切り文字へ依存しないテストへ変更する
- 最後に全体の回帰テストを実行する
PowerShell 7.6.4で重要なのは、コマンドが動くかどうかだけではありません。Join-Pathの型契約、WildcardPattern.Escape()の正しい戻り値、GetHelpCommandの正規化されたソース名まで確認する必要があります。
既存テストが失敗した場合は、まず製品コードの不具合と判断するのではなく、System.String、単独バッククォート、末尾空白付きソース名といった旧仕様の期待値が残っていないかを確認してください。3点を明示的な回帰テストへ追加してからPowerShell 7.6.4を展開すれば、更新後の予期しないテスト失敗を事前に防げます。

コメント