コレクションの大きさはプロパティLengthで取得するべきか、それともMeasure-Objectか?

PowerShellといえばオブジェクト・パイプで、オブジェクト・パイプといえばオブジェクトのコレクションだ。コマンドレットにはコレクションを返すものも多いし、それをパイプ経由でForEach-ObjectやWhere-Objectで処理することも多々ある。

ところで、時々ある条件に一致したデータの件数を調べたい時がある。

例えばSetで始まるコマンドレットを単純に列挙する場合はこうなる。

Get-Command | ?{$_ -cmatch "^Set"}

では、Setで始まるコマンドレットの数を取得したい場合はどうか?

真っ先に思いつくのは「コレクションが返ってくるのだから、そのプロパティLengthを参照する」というものだろう。

(Get-Command | ?{$_ -cmatch "^Set"}).Length

手元の環境*1ではうまく動作していて、19が返される。ネットの情報を見る限り、この方法は見当違いではなさそうだ。

ところがこの方法、実際に使ってみると都合が悪い時がある。例えば血迷ってZetで始まるコマンドレットの数を取得しようとしたとする。手元の環境にはそんな名前のコマンドレットは存在しないので、0が返ってくることが期待される。

(Get-Command | ?{$_ -cmatch "^Zet"}).Length

しかし実際には0は返ってこない。デフォルトでは、返ってくるのは$nullだ。

Where-Objectでデータを絞り込んだ時、該当件数が0の場合に返される値は何か? 空のコレクション/配列ではなく$nullだ。それがPowerShellの仕様として正しいか否かは不明だが、手元の環境での動作上はそうなっている。他のコマンドレットも多かれ少なかれ似たような感じのようで、例えば空のフォルダ内でGet-ChildItemを実行した時の値も$nullだった。

何となく$nullはプロパティを持っていないように思うので*2、直感的に$null.Lengthを取得しようとすると実行時エラーになる気がするのだが、デフォルトではエラーにはならず$nullが返ってくる。

この辺りの挙動はSet-StrictModeの設定によって異なる。デフォルト(設定OFF)ないしVersion 1.0では、存在しないプロパティを参照すると$nullが返される。Version 2.0ないしLatestなら実行時エラーになる。

さて、スクリプトを書いている身としては、該当データが0件の時に$nullが返ってくる挙動は少々都合が悪い。例えば次のコードのようにデータの件数によって分岐するケースを想定してみる。

$nmatch = (Get-Command | ?{$_ -cmatch "^Zet"}).Length
if ($nmatch -eq 0) {
    'No such command'
}

Zetで始まるコマンドレットが無かったと仮定する。デフォルトでは$nmatchの中身は$nullとなり、「$nmatch -eq 0」の評価値はFalseとなる。 'No such command' が返されることを期待しているコードだが、実際には返されることはない。

Set-StrictModeで2.0が設定されている場合は、プロパティLengthが存在しない為に実行時エラーとなってしまう。

ではどうすれば良いか? 実はMeasure-Objectというコマンドレットがあって、これを使うとデータ件数が0の場合でも正しく件数を取得できる。実際、次のコードなら0件の時に常に 'No such command' が返される。

$nmatch = (Get-Command | ?{$_ -cmatch "^Zet"} | Measure-Object).Count
if ($nmatch -eq 0) {
    'No such command'
}

measureというエイリアスもあるので、そちらを使ってもよいだろう。

そんな訳で、自分で明示的に配列を定義して操作しているならともかく、パイプ経由で処理した結果を扱う場合は、プロパティLengthではなくMeasure-Objectを使ってサイズを取得した方が無難なようだ。

*1:Windows XP Pro SP3にPowerShell 2.0を入れている。

*2:気になってGet-Memberで調べようとしたが、どう頑張っても実行時エラーになってしまった。