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つとなります。

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

まずは、パイプを利用するかどうかです。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:\tmp> (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:このファイルはテストです。

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

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

コメント