PowerShellのCompare-Object(diff)の罠

PowerShellでdiff相当のCompare-Objectを使って、差分だけ処理するスクリプトを書こうとしたら、PowerShellの仕様による罠にハマった。

要旨

Compare-Objectで比較した結果は、差異の個数によって型が変わる。

  • 差異がなかった(0個)の場合、$nullが返る
  • 差異が1個の場合、単一のPSCustomObject が返る
  • 差異が2個以上の場合、PSCustomObject配列が返る

この挙動の違いにより、結果を変数に保存したり、パイプにつないだりする場合に注意が必要になる。

必ず配列を返して欲しい場合は@()で囲むのが安全。

実際の挙動

環境はWindows 10でPowerShellのバージョンは5.1。

まずは差異の個数が0、1、2となるよう、Compare-Objectで比較する。

PS T:\> $diff0 = Compare-Object @(1,2,3) @(1,2,3)
PS T:\> $diff1 = Compare-Object @(1,2,3) @(1,2)
PS T:\> $diff2 = Compare-Object @(1,2,3) @(1)

上で説明したように、$diff0$nullが、$diff1PSCustomObjectが、$diff2は配列が返ってくる。

PS T:\> $diff0
PS T:\> $diff1

InputObject SideIndicator
----------- -------------
          3 <=

PS T:\> $diff2

InputObject SideIndicator
----------- -------------
          2 <=
          3 <=

$diff1$diff2の違いはコンソールに出力するとわかりにくいので、getType()を使って確認。

PS T:\> $diff0.getType()
null 値の式ではメソッドを呼び出せません
発生場所 :1 文字:1
+ $diff0.getType()
+ ~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) []RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

PS T:\> $diff1.getType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

PS T:\> $diff2.getType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

このため、Lengthプロパティを使って差異があるかどうかを判定するのは上手くいかない(もちろん、Lengthプロパティを使わず、$diff0 -neq $nullのようにすれば、差分があるかどうか自体は判定可能)。

# 差分が1個のときだけLengthプロパティが無い(なぜか$nullにLengthプロパティがある)
PS T:\> $diff0.Length
0
PS T:\> $diff1.Length
PS T:\> $diff2.Length
2

# 差分があるかどうかを調べたいときは-ne 0を使えば一応正しく判定可能
PS T:\> $diff0.Length -ne 0
False
PS T:\> $diff1.Length -ne 0
True
PS T:\> $diff2.Length -ne 0
True

# 同じように見えて-gt 0は使えない
PS T:\> $diff0.Length -gt 0
False
PS T:\> $diff1.Length -gt 0
False
PS T:\> $diff2.Length -gt 0
True

対処法としては、Compare-Objectの結果を@()で囲んで、必ず配列が返ってくるようにするのが安全。

PS T:\> $diff0_2 = @(Compare-Object @(1,2,3) @(1,2,3))
PS T:\> $diff1_2 = @(Compare-Object @(1,2,3) @(1,2))
PS T:\> $diff2_2 = @(Compare-Object @(1,2,3) @(1))

# GetType()で必ず配列が返る
PS T:\> $diff0_2.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array


PS T:\> $diff1_2.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array


PS T:\> $diff2_2.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

# Lengthプロパティも必ず存在する
PS T:\> $diff0_2.Length
0
PS T:\> $diff1_2.Length
1
PS T:\> $diff2_2.Length
2

参考