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命令。

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关键字,不是一个命令,也不是一个函数。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

现在,数组已创建,接下来需要确认每个服务器是否在线。一个很好的用于测试服务器连接的命令是Test-Connection。这个命令会在计算机上执行一些连接测试,以查看它是否在线。

通过在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方法

ForEach PowerShell 示例

让我们看一下如何使用ForEach循环的一些例子。这些例子基于实际的用例,展示了你可以修改以适应你的需求的概念。

示例1:使用ForEach语句在目录中的每个子文件夹中创建文件

此示例演示了PowerShell在目录中循环处理每个文件夹的常见用法。

假设在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命令来尝试启动每个服务。
  • 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命令处理它们
$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命令,导入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/