パイプからの入力を処理するForEach-Object
これまでも何度か紹介したように、PowerShellではパイプラインでオブジェクトを入出力します。コマンドレットがオブジェクト群をまとめて受け取ったとき、そのオブジェクト1つ1つに処理を実行したいケースは少なくありません。そんなとき便利なのがForEach-Objectコマンドレットです。基本的な書式は以下の通りです。
ForEach-Object -Begin {前処理} -Process {逐次処理} -End {後処理}
逐次処理は1つ1つのオブジェクトに対する処理、前処理は逐次処理に入る前に一度だけ行う処理、後処理は逐次処理がすべて終了した後に一度だけ行う処理です。前処理と後処理は省略することができます。
逐次処理内では、入力された個々のオブジェクトを「$_」変数で使用します。
たとえば、以下の記述では、1、2、3、4の4つのオブジェクトをパイプラインでForEach-Objectに送り、ForEach-Objectではそれぞれのオブジェクトを$_変数で3倍にしています。
1,2,3,4 | ForEach-Object -Begin {"3倍にします" } -Process { $_ * 3 } -End { "実行終わり"}
前処理や後処理がない場合には、オプション指定を省略して、以下のように記述することもできます。
ForEach-Object {逐次処理}
※ UNIX系OSのフィルタコマンドであるawkを使った経験があれば、awkの活用に似ていることに気づくでしょう。ただし、awkは行単位で繰り返し処理をするのに対して、ForEach-Objectはオブジェクト単位で繰り返し処理をします。
ファイル情報を処理
例として、ファイルのフルパスを表示してみます。
Get-ChildItemが出力するオブジェクト(この場合はファイル・フォルダ情報)には、Fullnameというプロパティがあります。そこで、Get-ChildItemからForEach-Objectに送り、$_.Fullnameを参照します。
Get-ChildItem | ForEach-Object { $_.Fullname }
なお、Get-ChildItemの省略形はgci、ForEachObjectの省略形は%です。以下のように簡略して記述できます。
gci | % { $_.Fullname }
以下の例では、カレントディレクトリのファイルの合計サイズを表示します。
gci | % -Begin { $sum = 0 } -Process { $sum += $_.Length } -End { $sum }
前処理として変数$sumを0に初期化し、逐次処理として各オブジェクトのLength(つまりファイルサイズ)を$sumに足し込みます。最後に$sumの計算結果を表示します。
オブジェクトを選択して処理
ForEach-Objectは入力されたすべてのオブジェクトに対して同じ処理を行いますが、Where-Objectでは指定した条件でオブジェクトを選択します。
Where-Object {選択条件となるスクリプト}
例1: フォルダだけを選択。PSIsContainerは、オブジェクトがコンテナ(フォルダ)の時にTrue(真)となるプロパティです。
gci | Where-Object { $_.PSIsContainer }
例2: 例1の条件を-not演算子で否定し、ファイルだけを選択。
gci | Where-Object { -not $_.PSIsContainer }
例3: 1MB以上のファイルサイズのファイルだけを選択。
gci | Whare-Object { $_.Length -ge 1MB}
例4: ファイル名の一文字目がa~z(アルファベット)のファイルだけを選択
gci | Where-Object { ($_.Name -match "^[a-z].*") -and (-not $_.PSIsContainer) }
ここでは、-match条件と、フォルダでない(-not $_.PSIsContainer)条件を-and(且つ)で合わせています。-matchは、文字列が条件に合うかどうかを判定する演算子で、正規表現を使用できます。-and演算子は-match演算子や-not演算子より優先順位が低いため、この式では()がなくても正しく働きます。しかし、プログラミングミスを防止するためには、()で計算の優先順位を明確にする習慣を付けるといいでしょう。
※ PowerShellの正規表現規則は.NET Frameworkの正規表現と同じです。正規表現に関してはインターネット等に豊富な情報があります。ワイルドカート度同様に文字列の条件判断をしますが、ワイルドカードより遙かに高度な条件を設定できます。"^[a-z].*"は、文字列の先頭がa~zの文字列であることを意味します。-match演算子では英字の大文字小文字を区別しません。-cmatch演算子は区別します。
さらにパイプラインで接続
Where-Objectはオブジェクト群を選択するだけなので、さらにパイプラインで他のコマンドレットに送ることで、応用範囲を広げられます。
例えば、以下の例では、サブディレクトリを含めて検索した20MB以上のファイルのフルパスを表示します。
gci -Recurse | Where-Object { $_.Length -ge 20MB } | % { $_.Fullname }
gci -Recurseでサブディレクトリを含めたファイル情報を収集します。Where-Objectでその中からサイズが20MB以上のものに選択します。最後に、%(ForEach-Object)で、選択したフルパスを表示します。
20MB以上のファイルを削除します。受け取ったオブジェクト群を一括して処理する機能を持つRemove-Itemのようなコマンドレットでは、%(ForEach-Object)を使う必要はありません。
gci -Recurse | Where-Object { $_.Length -ge 20MB } | Remove-Item
こうした手法は、Remove-Itemの代わりにCopy-Item、Move-Item、Rename-Itemなどを使って、様々に応用できます。また、レジストリプロバイダで条件に合うレジストリキーを削除したり、Get-ChildItemではなく、Get-ServiceやGet-Processを使って稼働中のプログラム情報を使って条件に合うプログラムを自動停止したり、応用できます。