PowerShell ForEach 循環:類型和最佳實踐

當您開始編寫 PowerShell 腳本時,您必然會遇到需要處理來自集合的多個項目的情況。這時您就需要深入了解 PowerShell 的 foreach 循環,了解它們的基本知識。

幾乎所有的編程語言都有一種被稱為循環的結構,PowerShell 也不例外。PowerShell 中最受歡迎的循環類型之一就是 foreach 循環。基本上,foreach 循環會讀取整個項目集合,並對每個項目運行某種代碼。

對於初學者來說,PowerShell 的 foreach 循環最令人困惑的一點是它具有多個選項。處理集合中的每個項目不只有一種方法,而是有三種!

在本文中,您將學習每種類型的 foreach 循環的工作原理以及何時使用其中一種。閱讀完本文後,您將對每種類型的 foreach 循環有一個良好的理解。

PowerShell ForEach 循環基礎知識

在 PowerShell 中,您將使用最常見的循環類型之一是 foreach 循環。foreach 循環會讀取一組對象(進行迭代),並在完成最後一個對象後結束。被讀取的對象集合通常由數組或哈希表表示。

注意:迭代是一個編程術語,指的是循環的每次運行。每次循環完成一個循環,這被稱為一次迭代。遍歷一組對象的循環運行常被稱為在該集合上進行迭代。

也許您需要在文件系統中的多個文件夾中創建一個文本文件。假設文件夾路徑是C:\FolderC:\Program Files\Folder2C:\Folder3。如果不使用循環,我們將不得不三次引用Add-Content cmdlet。

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中,技術上有三種類型的foreach循環。儘管每種類型的使用方式相似,但了解其區別非常重要。

foreach語句

第一種類型的foreach循環是一個語句foreach是PowerShell的內部關鍵字,不是一個cmdlet或函數。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 cmdlet的一种更快的替代方法。

ForEach-Object CmdLet

如果foreach是一个语句,只能以一种方式使用,那么ForEach-Object是带有参数的cmdlet,可以以许多不同的方式使用。就像foreach语句一样,ForEach-Object cmdlet可以迭代一组对象。只是这次,它将该组对象和要对每个对象执行的操作作为参数传递,如下所示。

$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 cmdlet有一个别名叫做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()方法,因為它的執行速度相對較快,尤其對於大型集合而言。

小型專案:迴圈遍歷一組伺服器名稱

在PowerShell中,迴圈最常見的用途之一是從某個來源讀取一組伺服器並對每個伺服器執行某些操作。對於我們的第一個小型專案,讓我們編寫一些程式碼,使我們能夠從文本檔案中讀取伺服器名稱(每行一個),並對每個伺服器進行ping測試,以確定它們是否在線。

List of server names in a text file

對於這個工作流程,你認為哪種類型的迴圈最適合使用?

請注意,我提到了「集合」這個詞,即「一組伺服器」。這是一個提示。你已經有了指定數量的伺服器名稱,並且想對每個伺服器執行某些操作。這聽起來就像是試用最常見的PowerShell迴圈的絕佳機會;foreach迴圈。

第一個任務是在代碼中創建一組服務器名稱。最常見的方法是創建一個數組。幸運的是,默認情況下,Get-Content會返回一個數組,數組中的每個元素由文本文件中的一行表示。

首先,創建一個名為$servers的包含所有服務器名稱的數組。

$servers = Get-Content .\servers.txt

創建了數組後,現在需要確認每個服務器是否在線。一個很好的測試服務器連接的cmdlet稱為Test-Connection。此cmdlet在計算機上執行幾個連接測試,以查看它是否在線。

通過在foreach循環中執行Test-Connection來讀取每個文件行,您可以將$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方法

PowerShell ForEach示例

讓我們來看幾個使用ForEach迴圈的例子。這些例子基於實際應用場景,展示了概念,您可以根據自己的需求進行修改。

例子1:使用ForEach語句在目錄中的每個子文件夾中創建文件

此示例演示了PowerShell中在目錄中使用ForEach遍歷文件夾的常見用法。

假設在C:\ARCHIVE_VOLUMES文件夾內有十個子文件夾。每個子文件夾表示每天備份的存檔卷。在每個文件夾內創建一個名為BackupState.txt的文件,其中包含備份完成的日期。

您可以在下面看到這可能是什麼樣子的例子。

Sub-directories under C:\ARCHIVE_VOLUMES

以下腳本執行三個操作:

  • 獲取C:\ARCHIVE_VOLUMES內所有子文件夾的列表
  • 遍歷每個文件夾
  • 在每個子文件夾內創建一個名為BackupState.txt的文本文件,其中包含當前日期和時間作為值

以下示例使用了foreach語句。

# 定義頂層文件夾
$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 cmdlet,您可以確認文件是否已在每個子文件夾內創建或更新。

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遍歷文件的用法,下面的腳本將讀取在示例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中創建多個用戶

在下一個示例中,假設您有一個具有兩列 – FirstnameLastname的CSV文件。然後,這個CSV文件應該填寫新建用戶的名字。CSV文件的內容應該如下所示。

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

現在進入下一個腳本區塊。首先,通過將內容路徑傳遞給 Import-CSV cmdlet 來導入 CSV 檔案。然後,使用 foreach() 方法遍歷名稱並在 Active Directory 中創建新用戶。

# 從 CSV 檔案中導入名字和姓氏列表
$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 迴圈,以及在使用哪一種時應該考慮哪些因素。您還看到了使用不同的示例場景下的三種 foreach 迴圈的示例。

進一步閱讀

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