しばたテックブログ

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

PowerShell 3.0での破壊的変更まとめ

以前のエントリで軽く触れたPowerShell 3.0における破壊的変更について一覧をまとめてみました。

blog.shibata.tech

元ネタはこちら(docx)

はじめに

blogs.msdn.microsoft.com

去年アナウンスされましたがPowerShell 2.0は既に非推奨です。
一応PowerShell 2.0のサポートはOSのサポートとセットになっているためOSがサポート切れにならない限りはPowerShell 2.0を使い続けても問題ないと言えますが、できることなら早めにWPowerShell 5.1(状況によってはPowerShell Coreへ)移行しておくと良いでしょう。

とくにクライアントOS(Windows 10)での利用においては、PowerShell 2.0が有効になっているとPowerShell 5.1に内蔵されているAMSIを回避するダウングレードアタックの対象ともなりセキュリティ的にも脆弱になるためできる限り新しいバージョンを使うべきです。

PowerShellの互換性

基本的にPowerShellのバージョンが上がる際の互換性は極力保たれる様になっているのですが、PowerShell 2.0から3.0においてはベースとなるランタイムが.NET Framework 2.0から.NET 4になったことやPowerShellの基本オブジェクトであるPSObjectの実装がDLRベースに差し替えられたこともあり、残念ながらそれなりに破壊的変更が生じています。

そしてその破壊的変更の一覧もリリースノート(docx)に軽く記載されているだけであまりきちんとした資料がありません。

本エントリではこの破壊的変更の一覧をまとめ軽く補足を入れていますのでPowerShell 2.0からの移行に役立てていただければと思います。

破壊的変更の一覧

1. finally ブロックでの return

PowerShell 2.0ではfinallyブロック内でreturnすることができましたが、PowerShell 3.0からこれはエラーとなります。

# OK : PowerShell 2.0
try {} finally { return }

# NG : PowerShell 3.0 ~
try {} finally { return }

f:id:stknohg:20181130131235j:plain

finallyブロックを抜けた後でreturn文を実行することでエラーを回避できます。

# Workaround : PowerShell 3.0 ~
try {
    # do something
}
finally {
    if ($cond) {
        $retval = 1
        $hasretval = $true
    }
}
if ($hasretval) {
    return $retval
}

2. 実行アセンブリ

PowerShell 2.0までのPowerShell.exeの実行アセンブリはSystem.Management.Automation.dllでしたが、これがPowerShell 3.0からは内部生成される動的なアセンブリに変更されます。

# PowerShell 2.0
PS C:\> [Reflection.Assembly]::GetExecutingAssembly() | Select-Object FullName

FullName
--------
System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35


# PowerShell 3.0 ~
PS C:\> [Reflection.Assembly]::GetExecutingAssembly() | Select-Object FullName

FullName
--------
Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

この変更により影響を受ける箇所はリリースノートでも

Various errors or incorrect behavior, depending on the use.

としか書かれていないため何とも言えません。
個人的にはリフレクションを扱うコードで影響を受けそうな雰囲気を感じるので、そういったコードを移行する際にはこの点に気を付けてみるのが良さそうです。

3. 演算子で使用される変数のスコープ

これは地味に嫌らしい変更になります。

加減乗除などの演算(+=, -=, *=, %=, ++, --といったものも含む)で読み書きされる変数は、PowerShell 2.0まではダイナミックスコープだったのがPowerShell 3.0からはローカルスコープになります。

# スコープの異なる $x 変数を使った例
$x = 5
& { $x += 1; echo $x }

# PowerShell 2.0では & { $x += 1; echo $x } は 6 を返す
PS C:\> & { $x += 1; echo $x }
6

# PowerShell 3.0以降は & { $x += 1; echo $x } は 1 を返す
PS C:\> & { $x += 1; echo $x }
1

4. 数値と文字列の比較

ちょっと細かい仕様まで調べきることが出来なかったのですが、PowerShell 3.0からは数値の文字列の等値比較の結果が異なります。
PowerShell 2.0まででは、たとえば"1.1"の様な文字列の数字を単純にキャストして比較しており、PowerShell 3.0からは"1.1"といった文字列が整数なのか小数なのかといった部分までを考慮して比較する様になっている感じです。

リリースノートの例を以下に転記しておきます。

# PowerShell 2.0
# ※おそらく単純に[int]型へのキャストをしている
PS C:\> 1 -eq "1.1"
True

# 1kbなどの単位付き文字列は数値変換できず
PS C:\> 1024 -eq "1kb"
False
# PowerShell 3.0 ~
# 文字列が小数であることが考慮されて比較される
PS C:\> 1 -eq "1.1"
False

# 1kb = 1024 といった内容が考慮して比較される
PS C:\> 1024 -eq "1kb"
True

5. System.Core.dll への参照

PowerShell 3.0からPSObjectを利用するC#のコードに対してSystem.Core.dllへの参照を追加する必要があります。

PSObject obj = val as PSObject;

6. Add-Typeの挙動

こちらは説明がしにくいのでリリースノートの記述をそのまま引用します。

Add-Member and PSTypeNames now apply to the underlying base object, not to PSObject

要は、PowerShell 3.0からはAdd-Memberの結果はPSObjectに内包されるBaseObjctに付くということなのですが、こちらは実際の例を見てもらう方が早いでしょう。

# PowerShell 2.0では以下の様な場合 $x.Test プロパティは何も返さない
PS C:\> $x = @{}
PS C:\> $x | Add-Member –MemberType NoteProperty -Name Test -Value 42
PS C:\> $x.Test

# 回避策として -PassThru パラメーターの指定が必要
PS C:\> $x = @{}
PS C:\> $x | Add-Member –MemberType NoteProperty -Name Test -Value 42 -PassThru
PS C:\> $x.Test
42

# PowerShell 3.0からは -PassThru パラメーターなしで $x.Test プロパティが設定される
PS C:\> $x = @{}
PS C:\> $x | Add-Member –MemberType NoteProperty -Name Test -Value 42
PS C:\> $x.Test
42

7. ストリームの追加による挙動の変化

PowerShell 3.0からWarning output stream(3>)Debug output stream(5>)のストリームが増え演算子となったことで一部のコマンドに対して実行結果が異なる様になりました。

たとえば以下のコードはPowerShell 3.0からはエラーとなります。

# PowerShell 3.0からエラー
echo 1> 1>> 3> 3>> 4> 4>> 5> 5>> 6> 6>>

ストリームの細かい話はこちらをご覧ください。

blog.shibata.tech

8. スクリプトブロックの IsFilter プロパティ

PowerShell 3.0から無名スクリプトブロックのIsFilterを設定するとエラーになります。

# PowerShell 3.0からは以下のコードはエラーとなる
$sb = {$_*5}
$sb.IsFilter = $true
$sb2.IsFilter

# 回避策としては名前付きスクリプトブロックを使用する
{process {$_*5}}

9. foreach文の列挙変数

PowerShell 3.0からはforeach文の列挙変数は独自のスコープを持たなくなりました。

# PowerShell 2.0では ($n in 'z') の$nが独自のスコープを持つためエラーにならない
[int[]]$n = 1,2,3
foreach ($n in 'z') {}

# PowerShell 3.0からは [int[]]$n を参照するためエラーとなる
[int[]]$n = 1,2,3
foreach ($n in 'z') {}

f:id:stknohg:20181201105500j:plain

10. 配列要素のプロパティに対するアクセス

こちらはPowerShell 3.0の新機能でも紹介されているものですが、PowerShell 3.0からは配列の各要素のプロパティに直接アクセスできる様になっています。

# Get-Processの結果は [System.Diagnostics.Process]型 の配列
PS C:\> $ps = Get-Process
PS C:\> $ps.Count
243

# PowerShell 3.0からは[System.Diagnostics.Process]型のNameプロパティにアクセスできる
PS C:\> $ps.Name
ApplicationFrameHost
armsvc
backgroundTaskHost
・・・(後略)・・・

# これは以下のコードと概ね等価
$ps | ForEach-Object { $_.Name }

11. スカラ値に対するインデックスアクセス

PowerShell 3.0からはスカラ値に対してもインデックスアクセス([0])が可能になっています。
PowerShell 2.0では実行時エラーとなります。

# PowerShell 3.0からはスカラ値に対してもインデックスアクセスが可能
PS C:\> $i = 123
PS C:\> $i[0]
123

12. 複数の #requires -version

PowerShell 3.0から複数のバージョンが異なる#requires -version文が記述されている場合はエラーになります。

#requires -version 2.0
#requires -version 3.0

13. デリゲート代わりに使われるスクリプトブロックのスコープ

PowerShell 3.0からデリゲート代わりに使われるスクリプトブロックが独自のスコープを持つようになっています。

たとえば以下の様な例だとPowerShell 2.0では1を、PowerShell 3.0からは0を返す様になります。

# PowerShell 2.0では 1、PowerShell 3.0からは 0 を返す
Add-Type @"
public class Invoker
{
    public static void Invoke(System.Action<int> func)
    {
        func(1);
    }
}
"@
$a = 0
[Invoker]::Invoke({$a = 1})
$a