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で調べようとしたが、どう頑張っても実行時エラーになってしまった。