Where-Objectのスクリプトブロックで複数の文を使いたいとき

へえこんなふうになるんだ

> 1..3 | ?{ $_ % 2 }
1
3
> 1..3 | ?{ $false }
> 1..3 | ?{ $true }
1
2
3
> 1..3 | ?{ $false; $false } # 注目
1
2
3

whereのスクリプトブロックで複数の文使えない!?

おさらい。where-objectの{ }は、フィルタースクリプトと書いてあるように、真偽値を返すスクリプトブロックである。

 Where-Object [-FilterScript] <scriptblock> [-InputObject <psobject>]  [<CommonParameters>]

ってことでスクリプトブロックの評価について調べてみた

> (&{ $false; $false }).GetType()

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

>  (&{ $false; }).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Boolean                                  System.ValueType

> [bool](&{ $false; $false })
True

なるほど、?{ $false; $false }だと、@($false, $false)という配列になってTrueと判定されるみたい

ちなみに配列を真偽値に変換すると?

> [bool] @()
False
> [bool] @($false)
False
> [bool] @($true)
True
> [bool] @($false, $false)
True

なるほど、複数の要素がある配列だとTrueなのかな?だから、?{ $false; $false }だとTrueになると。


では、こうしたい時はどうすればよいか?ファイル名末尾の番号が235以上のものを取得したい。

> gci -file | ?{ $_.Name -match "\d+$"; [int]$matches[0] -ge 235 } # スクリプトブロックがTrueになるので、全ファイルが取得されてしまう!

and演算子を使えばうまくいった

> gci -file | ?{ ($_.Name -match "\d+$") -and ([int]$matches[0] -ge 235) }

()内部が式ではなく文のときもandでうまくいくかは謎だし、andの前の項がTrueを返さないといけなくて微妙

これでもうまくいった。こっちのほうがいいかも

> gci -file | ?{ $( $_.Name -match "\d+$"; [int]$matches[0] -ge 235 )[1] }

$()による部分式は内部で複数の値を返すときに配列になる。たくさんの文を含むときは$(...)[-1}とすれば文の数を数えずに済む

追記5/3 16:25
void型にキャストすれば明示的にコマンドラインの結果を破棄できると知った

> 1..3 | ?{ [void]$false; $_ % 2 }
1
3
> @( "a"; [void]"b"; "c" )
a
c

これを使って次のようにすればうまくいった。文全体をvoidでキャストする点に注意

> gci -file | ?{ [void]($_.Name -match "\d+$"); [int]$matches[0] -ge 235 }

しかし、たくさんの文を含むときはいちいち[void]するのが面倒そうなので、やはり$(...)[-1]の方法も覚えておきたい