PowerShell の foreach (ForEach-Object) の使い方

Powershell

PowerShell では foreach が 2 種類あります。それぞれ特徴があるので、使い分けと使い方について紹介します。

使い方の詳細のみ知りたい方は目次より飛んでください。

foreach ステートメントと ForEach-Object コマンドレットの概要

少し分かりづらいのですが、PowerShell における foreach によるループは 2 種類あり、それぞれ構文の書き方や動作が異なります。

foreach ステートメント

1つ目が foreach ステートメントです。以下簡単な例です。

PS C:\>$array = @(1,2,3,4,5) PS C:\>foreach($a in $array){ >> Write-Host $a  >> } 1 2 3 4 5

基本的には配列に格納されている値やオブジェクトを一つずつ処理する際に使用します。変数のBaseType が System.Array となっている変数に対して利用可能です。

PS C:\> $array.GetType() IsPublic IsSerial Name      BaseType -------- -------- ----      -------- True     True     Object[]  System.Array

ForEach-Object コマンドレット

2つめは ForEach-Object コマンドレットです。以下、簡単な例です。

PS C:\> @(1,2,3,4,5) | ForEach-Object{Write-Host $_} 1 2 3 4 5 PS C:\>@(1,2,3,4,5) | foreach{Write-Host $_#同じ結果が得られる PS C:\>@(1,2,3,4,5) | %{Write-Host $_#同じ結果が得られる

基本的にはパイプでオブジェクトを受け取って処理を実行します。こちらについても受け取るオブジェクトが配列である必要があります。

foreach ステートメントと ForEach-Object コマンドレットの使い分け

非常にややこしいのは、ForEach-Object の Alias (別名) に foreach という名前があるためです。

PS C:\> Get-Alias | Where-Object{$_.DisplayName -like "*ForEach-Object"} CommandType     Name                        Version    Source -----------     ----                        -------    ------ Alias           % -> ForEach-Object Alias           foreach -> ForEach-Object

ForEach-Object は既定で別名として「foreach」「%」が登録されています。そのため、「foreach」で動く構文が2種類存在してしまっています。

どちらを利用するかの判断ポイントは大まかに以下の2つとなります。

  1. パイプを利用するかどうか
  2. 処理するデータの量と求められるスピード、メモリ容量

まずは、パイプを利用するかどうかです。foreach ステートメントはパイプでオブジェクトの受け取りが不可のため、パイプ処理を利用する場合は ForEach-Object を利用する必要があります。とはいえコードの書き方次第でパイプは利用しない構成にも出来るので、これは好みの問題となります。

もうひとつは内部処理による違いです。foreach ステートメントと ForEach-Object ではループでのメモリの利用方法に差異があります。

具体的には foreach ステートメントは受け取った配列の要素を全てメモリに読み込んでから処理が実行されます。そのため、処理内容によっては大量のメモリを消費する可能性がありますが、その分高速になる可能性があります。

一方、ForEach-Object では受け取ったオブジェクトを一つずつメモリに読み込んで処理が実行されます。そのため、大量のオブジェクトがあると、処理ごとにオブジェクトの生成と破棄が行われるため、利用するメモリの量は少ないですが処理が遅くなる可能性があります。

簡単なスクリプトであればあまり考慮する必要はないですが、大量のデータを処理するスクリプトなどでは考慮が必要になってきます。状況によって判断、というしかありません。

foreach ステートメントの使い方

foreach ステートメントの使い方です。

foreach( [変数] in [配列] ) {
    処理内容
}

配列の要素をひとつずつ変数に格納して処理を実行します。

以下、foreach ステートメントを利用した例です。特定のフォルダ内 (この例では C:\Tmp) のファイルに、特定の文字列が含まれているか検索するスクリプトです。

  • C:\Tmp\aaa.txt
    ⇒ファイルの中身:このファイルはテストファイルです。
  • C:\Tmp\bbb.txt
    ⇒ファイルの中身:このファイルはテキストファイルです。
  • C:\Tmp\ccc.txt
    ⇒ファイルの中身:このファイルはテストです。
# C:\foreach.ps1 
$Array = (Get-ChildItem -Path C:\Tmp -File).FullName  #C:\tmpの中のファイル一覧を$Arrayに格納 
foreach($a in $Array){  #Arrayの要素を変数$aに格納して処理を実行 
  If(Select-String -Path $a -Pattern "テスト" -Quiet){  #テストという文字列が含まれていればTrueとなる 
    Write-Host $a にはテストという文字列が含まれています。 
  } 
}
PS C:\> C:\foreach.ps1 C:\Tmp\aaa.txt にはテストという文字列が含まれています。 C:\Tmp\ccc.txt にはテストという文字列が含まれています。

ForEach-Object の使い方

ForEach-Object の使い方です。

[配列オブジェクト] または [各種コマンドレット] | ForEach-Object{ 処理内容 }

foreach ステートメントで利用した例を ForEach-Object でも書いてみます。

PS C:\> (Get-ChildItem -Path C:\Tmp -File).FullName | ForEach-Object{ If(Select-String -Path $_ -Pattern "テスト" -Quiet){ Write-Host $_ にはテストという文字列が含まれています。 } } C:\Tmp\aaa.txt にはテストという文字列が含まれています。 C:\Tmp\ccc.txt にはテストという文字列が含まれています。

一行で書けました。

「(Get-ChildItem -Path C:\Tmp -File).FullName」という Get-ChildItem コマンドレットのFullName プロパティを取得すると、この例では以下の値が取得されます。

>PS C:\> (Get-ChildItem -Path C:\Tmp -File).FullName C:\Tmp\aaa.txt C:\Tmp\bbb.txt C:\Tmp\ccc.txt

この3つ要素が格納されている配列を一つずつパイプの先に渡します。

渡した先では「$_」という特殊な変数に格納されるので、この変数に対してファイルの中身を検索する処理を追加しています。

また、ForEach-Object には -Begin や -End といったオプションがあり、処理が実施される前または処理が実施された後に実行する処理を指定することもできます。但し、このオプションを使うくらいならforeach ステートメントの方が他の言語に通ずる書き方でもあるため、可読性が高いです。本記事の前半で紹介したよう、処理のスピードやメモリについての制約がなければ、foreach ステートメントで書くのがいいかと思います。

余談

例で特定フォルダ内のファイルに、特定の文字列が含まれているか検索するスクリプトを紹介しましたが、同じようなことが Select-String コマンドレットのみで可能です。

PS C:\> Select-String -Path C:\tmp\*.txt -Pattern テスト aaa.txt:1:このファイルはテストファイルです。 ccc.txt:1:このファイルはテストです。

頑張ってスクリプトを作って思い通りに動作したけど、実はこんな書き方があったんだ、ということがよくあります。(この例は露骨ですが)

作りこむ前にしっかりと情報収集することをおすすめします。

コメント

タイトルとURLをコピーしました