PowerShell ForEachループ:タイプとベストプラクティス

最初にPowerShellスクリプトの作成を始めると、コレクションから複数のアイテムを処理する必要がある場面に遭遇することがあります。その時には、PowerShellのforeachループについて深く掘り下げて学ぶ必要があります。

ほとんどのプログラミング言語には、ループと呼ばれる構造がありますが、PowerShellも例外ではありません。PowerShellで最も人気のあるループの一つは、foreachループです。基本的に、foreachループはコレクション全体を読み込み、それぞれのアイテムごとにコードを実行します。

PowerShellのforeachループについて、初心者にとって最も混乱するのは、利用可能なオプションの多さです。コレクション内の各アイテムを処理するための方法は一つだけではありません。実際には、3つの方法があります!

本記事では、各タイプのforeachループの動作方法と、どのタイプを使うべきかについて学びます。この記事を読み終える頃には、各タイプのforeachループについて理解が深まるでしょう。

PowerShellのForEachループの基本

PowerShellでよく使用するループの一つは、foreachループです。foreachループは、オブジェクトのセット(イテレーション)を読み込み、最後のオブジェクトで処理が完了するまで続きます。読み込まれるオブジェクトのコレクションは通常、配列やハッシュテーブルで表されます。

注意:イテレーションという用語は、ループの各実行を指すプログラミング用語です。ループが1サイクル完了するたびに、これはイテレーションと呼ばれます。オブジェクトのセット上でループを実行する行為は、一般にセットをイテレートすると言われます。

おそらく、ファイルシステム上に広がるいくつかのフォルダにテキストファイルを作成する必要があるかもしれません。たとえば、フォルダのパスはC:\FolderC:\Program Files\Folder2C:\Folder3とします。ループを使用しない場合、Add-Contentコマンドレットを3回参照する必要があります。

Add-Content -Path 'C:\Folder\textfile.txt' -Value 'This is the content of the file'
Add-Content -Path 'C:\Program Files\Folder2\textfile.txt' -Value 'This is the content of the file'
Add-Content -Path 'C:\Folder2\textfile.txt' -Value 'This is the content of the file'

これらのコマンド参照の唯一の違いは何ですか?それはPathの値です。各参照間で変化するのはPathの値だけです。

コードを多重化しています。入力するための時間を無駄にしており、将来の問題を引き起こす可能性があります。代わりに、変化しているすべてのアイテムを含む「セット」を作成するべきです。この例では、配列を使用します。

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')

これで各パスが単一の「セット」または配列に格納されました。これで、各要素を繰り返し処理するためのループを使用できる状態になりました。ただし、その前に、PowerShellを初めて使う人々が通常つまづくトピックについて説明するのが良いタイミングです。他の種類のループとは異なり、foreachループは同じものではありません。

厳密に言えば、PowerShellには3つの異なるforeachループのタイプがあります。使用方法は似ていますが、違いを理解することが重要です。

foreachステートメント

最初のタイプのforeachループはステートメントです。 foreachは、コマンドレットでも関数でもない内部のPowerShellのキーワードです。 foreachステートメントは常にforeach ($i in $array)の形式で使用されます。

上記の例では、$i変数はイテレータを表し、$pathの各要素を反復処理する際の各アイテムの値を表します。

イテレータ変数の名前は$iである必要はありません。変数名は任意のものにすることができます。

以下の例では、Add-Contentの参照を繰り返すという同じタスクを次のように実行することができます:

# フォルダの配列を作成
$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')

# 各フォルダに同じファイルを作成するための反復処理を実行
foreach ($i in $folders) {
    Add-Content -Path "$i\SampleFile.txt" -Value "This is the content of the file"
}

foreach ステートメントは、ForEach-Object コマンドレットを使用するよりも高速な代替手段であることが知られています。

ForEach-Objectコマンドレット

foreachはステートメントであり、単一の方法で使用することしかできませんが、ForEach-Objectはパラメータを持つコマンドレットであり、さまざまな方法で使用することができます。 foreachステートメントと同様に、ForEach-Objectコマンドレットは、オブジェクトのセットを反復処理します。ただし、この場合は、そのオブジェクトのセットと各オブジェクトに対して実行するアクションをパラメータとして渡します。

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')
$folders | ForEach-Object (Add-Content -Path "$_\SampleFile.txt" -Value "This is the content of the file")

注意:混乱を招くため、ForEach-Objectコマンドレットにはforeachというエイリアスがあります。用語「foreach」がどのように呼ばれるかによって、foreachステートメントが実行されるか、ForEach-Objectが実行されます。

A good way to differentiate these situations is to notice if the term “foreach” is followed by ($someVariable in $someSet). Otherwise, in any other context, the code author is probably using the alias for ForEach-Object.

foreach()メソッド

最新のPowerShell v4で導入されたforeachループの一つは、foreach()メソッドと呼ばれます。このメソッドは、配列やコレクションオブジェクトに存在します。foreach()メソッドには、他のメソッドと同様に、各イテレーションで実行するアクションを含む標準のスクリプトブロックパラメータがあります。

$folders = @('C:\Folder','C:\Program Files\Folder2','C:\Folder3')
$folders.ForEach({
	Add-Content -Path "$_\SampleFile.txt" -Value "This is the content of the file"
})

foreach()メソッドの最も重要な違いは、内部での動作方法です。

大規模なデータセットの場合、foreach()メソッドを使用するとかなり高速で、明らかな差があります。可能な場合は、他の2つの方法よりもこのメソッドを使用することが推奨されます。

ミニプロジェクト:サーバー名のセットに対するループ処理

PowerShellでループを使用する最も一般的な用途の一つは、あるソースからサーバー名のセットを読み取り、それぞれのサーバーに対して何らかのアクションを実行することです。最初のミニプロジェクトでは、テキストファイルから1行ずつサーバー名を読み取り、各サーバーにpingを実行してオンラインかどうかを判定するコードを作成します。

List of server names in a text file

このワークフローでは、どのタイプのループが最適だと思いますか?

「セット」という言葉を使用したことに注目してください。「サーバーのセット」ということです。すでに指定された数のサーバー名があり、それぞれに何らかのアクションを実行したいと考えています。これは、最も一般的に使用されるPowerShellのループであるforeachループを試す絶好の機会です。

最初のタスクは、コード内でサーバー名のセットを作成することです。最も一般的な方法は、配列を作成することです。幸運なことに、Get-Contentはデフォルトで、テキストファイルの各行を要素とする配列を返します。

まず、すべてのサーバー名の配列を作成し、$serversと呼びます。

$servers = Get-Content .\servers.txt

配列が作成されたので、各サーバーがオンラインかどうかを確認する必要があります。サーバーの接続をテストするための素晴らしいコマンドレットがあります。それはTest-Connectionと呼ばれます。このコマンドレットは、コンピューターのオンライン/オフラインを確認するために、いくつかの接続テストを実行します。

Test-Connectionforeachループの中で実行し、各ファイル行を読み取ることで、$server変数で表される各サーバー名をTest-Connectionに渡すことができます。これがPowerShellでテキストファイルをループする方法です。以下に、これがどのように機能するかの例を示します。

foreach ($server in $servers) {
	try {
		$null = Test-Connection -ComputerName $server -Count 1 -ErrorAction STOP
		Write-Output "$server - OK"
	}
	catch {
		Write-Output "$server - $($_.Exception.Message)"
	}
}

foreachループが実行されると、以下のようになります。

Looping Test-Connection through a list of server names

これで、サーバー名が記載されたテキストファイルの接続を正常にテストしました!この時点で、コードを変更することなく、テキストファイル内のサーバー名を自由に追加または削除することができます。

私たちが説いていることを実践することに成功しました、DRYメソッドです。

ForEach PowerShellの例

ForEachループの使い方のいくつかの例を見てみましょう。これらは、コンセプトを示す現実世界のユースケースに基づいており、必要に応じて変更できます。

例1:ForEachステートメントを使用してディレクトリ内の各サブフォルダにファイルを作成する

この例では、PowerShellのforeachフォルダー内の共通の使用方法を示しています。

仮に、C:\ARCHIVE_VOLUMESフォルダ内に10個のサブフォルダがあるとします。各サブフォルダは、毎日バックアップされるアーカイブボリュームを表します。バックアップが完了すると、BackupState.txtという名前のファイルが作成され、バックアップされた日付が含まれます。

以下に、これがどのように見えるかの例を示します。

Sub-directories under C:\ARCHIVE_VOLUMES

以下のスクリプトは、次の3つのアクションを実行します:

  • C:\ARCHIVE_VOLUMES内のすべてのサブフォルダのリストを取得する
  • 各フォルダをループ処理する
  • 現在の日付と時刻の値を含むBackupState.txtという名前のテキストファイルを作成する

以下の例では、foreachステートメントを使用しています。

# TOPレベルフォルダを定義する
$TOP_FOLDER = "C:\ARCHIVE_VOLUMES"

# 再帰的にすべてのサブフォルダを取得する
$Child_Folders = Get-ChildItem -Path $TOP_FOLDER -Recurse | Where-Object { $_.PSIsContainer -eq $true }

# 各サブフォルダにテキストファイルを作成し、現在の日時を値として追加する
foreach ($foldername in $Child_Folders.FullName) {
   (get-date -Format G) | Out-File -FilePath "$($foldername)\BackupState.txt" -Force
}

Get-ChildItemコマンドレットを使用して、サブフォルダの各内部にファイルが作成または更新されたかどうかを確認できます。

Get-ChildItem -Recurse -Path C:\ARCHIVE_VOLUMES -Include backupstate.txt | Select-Object Fullname,CreationTime,LastWriteTime,Length

以下のスクリーンショットは、スクリプトの出力を示しており、それぞれのサブディレクトリで見つかったすべてのBackupState.txtファイルを表示しています。

A text file is created in each sub-directory

例2:サブディレクトリの各テキストファイルの内容を読み取る

次に、PowerShellのforeachファイルを使用してディレクトリ内の各BackupState.txtファイルを読み取るために、以下のスクリプトは、例1で作成された各BackupState.txtファイルを読み込みます。

  • 各サブディレクトリ内のすべてのBackupState.txtファイルを再帰的に検索します。
  • foreachステートメントを使用して、各テキストファイルを読み取り、「最終バックアップ時刻」の値を取得します。
  • 結果を画面に表示します。
## C:\ARCHIVE_VOLUMES内のすべてのBackupState.txtファイルを検索する
$files = Get-ChildItem -Recurse -Path C:\ARCHIVE_VOLUMES -Include 'BackupState.txt' | Select-Object DirectoryName,FullName

## 各ファイルの内容を読み取る
foreach ($file in $files) {
    Write-Output ("$($file.DirectoryName) last backup time - " + (Get-Content $file.FullName))
}

このスクリプトをPowerShellセッションで実行すると、以下のスクリーンショットと似たような出力が表示されます。これにより、PowerShellがファイルをループし、内容を読み取り、出力を表示することが示されます。

Using foreach to loop through files and read its contents.

例3:ForEach-Object CmdLetを使用してサービスを取得し、起動する

システム管理者は、しばしばサービスの状態を取得し、失敗したサービスを修復するための手動または自動のワークフローをトリガーする必要があります。次に、ForEach-Object cmdletを使用したサンプルスクリプトを見てみましょう。

この例では、以下のスクリプトが以下の操作を行います:

  • 自動起動に設定されているが現在実行されていないサービスのリストを取得します。
  • 次に、リスト内のアイテムはForEach-Object cmdletにパイプされ、各サービスの起動を試みます。
  • A message of either success or failed is displayed depending on the result of the Start-Service command.
## 停止している自動サービスのリストを取得します。
$services = Get-Service | Where-Object {$.StartType -eq 'Automatic' -and $.Status -ne 'Running'}

## 各サービスオブジェクトをパイプラインに渡し、ForEach-Object cmdletで処理します。
$services | ForEach-Object {
    try {
        Write-Host "Attempting to start '$($.DisplayName)'"
        Start-Service -Name $.Name -ErrorAction STOP
        Write-Host "SUCCESS: '$($.DisplayName)' has been started"
    } catch {
        Write-output "FAILED: $($.exception.message)"
    }
}

スクリプトを実行すると、以下のスクリーンショットのような出力が表示されます。各サービスには起動コマンドが発行されていることがわかります。いくつかは正常に起動されましたが、いくつかは起動に失敗しました。

Using the ForEach-Object loop to start services

例4: ForEach() メソッドを使用してCSVからデータを読み取る

CSVファイルからデータを使用するのはシステム管理者の間で一般的です。CSVファイル内のレコードを使用すると、Import-CSVForEachの組み合わせを使用して一括操作を簡単に実行できます。Active Directoryで複数のユーザーを作成するために、この組み合わせは一般的に使用されます。

次の例では、2つの列(FirstnameLastname)を持つCSVファイルがあることを前提としています。このCSVファイルには作成する新しいユーザーの名前が記載されているはずです。CSVファイルは以下のようになります。

"Firstname","Lastname"
"Grimm","Reaper"
"Hell","Boy"
"Rick","Rude"

次のスクリプトブロックに移りましょう。まず、Import-CSVコマンドレットにコンテンツのパスを渡してCSVファイルをインポートします。その後、foreach()メソッドを使用して名前をループ処理し、Active Directoryに新しいユーザーを作成します。

# CSVファイルからFirstnameとLastnameのリストをインポートする
$newUsers = Import-Csv -Path .\Employees.csv

Add-Type -AssemblyName System.Web

# リストを処理する
$newUsers.foreach(
    {
        # ランダムなパスワードを生成する
        $password = [System.Web.Security.Membership]::GeneratePassword((Get-Random -Minimum 20 -Maximum 32), 3)
        $secPw = ConvertTo-SecureString -String $password -AsPlainText -Force

        # ユーザー名を作成する
        $userName = '{0}{1}' -f $_.FirstName.Substring(0, 1), $_.LastName

        # 新しいユーザーの属性を構築する
        $NewUserParameters = @{
            GivenName       = $_.FirstName
            Surname         = $_.LastName
            Name            = $userName
            AccountPassword = $secPw
        }

        try {
            New-AdUser @NewUserParameters -ErrorAction Stop
            Write-Output "User '$($userName)' has been created."
        }
        catch {
            Write-Output $_.Exception.Message
        }
    }
)

実行すると、CSVファイルの各行に対してADユーザーが作成されます!

概要

PowerShellのforeachループのロジックは、他のプログラミング言語とは変わりません。ただし、使用方法や特定のタスクに対してどのバリエーションのforeachループを選択するかは異なります。

この記事では、PowerShellで使用できるforeachループの異なるタイプと、どのタイプを使用するかについて学びました。また、さまざまなサンプルシナリオを使用して3つのforeachループのタイプを実際に見てきました。

さらに読む

Source:
https://adamtheautomator.com/powershell-foreach/