The foreach statement in PowerShell is an iteration statement which allows you to repeatedly execute a command or script against each item in a collection or array. This enables you to speed up processing for large collections of items where the same process or function needs to run against each item.
In this tutorial, I am going to go into to detail to explain the usefulness of using foreach statements within your PowerShell scripts and provide you practical examples where this may come in useful.
How is a Foreach loop useful
Without a foreach loop, performing similar actions more than once on multiple items would be tedious and inefficient. not only would be time-consuming to write the same commands over and over again, but it would be an inefficient use of code. The foreach statement solves this problem.
Simply put, you can use a foreach statement to turn a piece of code like this:
$report = [System.Collections.Generic.List[Object]]::new()
$report += Get-Service -name BITS -RequiredServices | Select Name, ServiceType, Status
$report += Get-Service -name DHCP -RequiredServices | Select Name, ServiceType, Status
$report += Get-Service -name gpsvc -RequiredServices | Select Name, ServiceType, Status
$report += Get-Service -name iphlpsvc -RequiredServices | Select Name, ServiceType, Status
Into something that looks a little more elegant:
$services = Get-Service
$report = [System.Collections.Generic.List[Object]]::new()
Foreach ($Service in $Services) {
$report += $service | Select Name, ServiceType, Status
}
Or even:
As you can see from the above, for what could have been 50 lines of code, it has been narrowed down to just 5 lines, or even just 2 lines if you want to be really efficient! Both of the improved examples work great, however, depending on the experience level of those reading your code, the first example (with 5 lines) may be easier to read and understand.
$report = [System.Collections.Generic.List[Object]]::new()
Get-Service | Foreach-Object {$report += $_ | Select Name, ServiceType, Status}
How to write a Foreach loop
Foreach loops have a basic structure when you write them. The foreach statement is followed by brackets, and within the brackets is the in operator. Looking at the below structure $item defines the single item in each cycle of the loop and $itemcollection is the variable that contains multiple items that are being looped through.
This is then followed by braces (or curly brackets) and within these braces are the commands which you can run against each item in the collection by using the $item variable.
Foreach ($item in $itemcollection) {
"Do something" $item
}
Foreach vs Foreach-Object
Although the foreach statement and foreach-object cmdlet look similar, they serve different purposes. The confusing bit is that foreach is also an alias of foreach-object, so although the intentions of each command are the same, they are used slightly differently.
Fundamentally, when using the foreach statement, your collection of items is stored in memory up front in a variable, such as in the above example, where before using the foreach statement, my items are saved in the variable $itemcollection. Where-as the foreach-object cmdlet is designed to be used in your command pipeline and although it takes longer to execute, it has more available parameters to use.
You can see the speed differences between the foreach statement and foreach-object cmdlet by using the Measure-Command command.
Let’s start by measuring the speed of running Foreach-Object in our pipe line.
Measure-Command {
$report = [System.Collections.Generic.List[Object]]::new()
Get-Service | Foreach-Object {$report += $_ | Select Name, ServiceType, Status}
}
As you can see it took a total of 72 milliseconds to run the command.
Now we will look at the speed when running the same command using the foreach statement.
Measure-Command {
$services = Get-Service
$report = [System.Collections.Generic.List[Object]]::new()
Foreach ($Service in $Services) {
$report += $service | Select Name, ServiceType, Status
}
}
As you can see, although in our scenario the time difference is minimal, the command was completed a lot quicker, in 47 milliseconds compared to using foreach-object.
Foreach loop Examples
Below are some different examples for using foreach loops with PowerShell.
Run the same command for multiple items
In the simplest form, using a foreach loop, you can run the same command against multiple items in a collection. In the below example, I have manually defined a collection of names or services found on a local computer. I then looped through each service and ran both the stop-service and start-service commands which will perform related actions on each service.
$Services = "Spooler","NetLogon","dbupdate","BITS"
Foreach ($service in $services) {
Stop-service $service
start-service $service
}
Modify settings for multiple items
Foreach loops can be used for more complex tasks, for example, as I work often in Active Directory, I may want to modify a single property across multiple users. In the below example, I store all users with a specific setting into the variable $users. I then loop through each user to modify that setting to a new value, in this case it is the user principal name setting.
$users = Get-ADUser -Filter * | Where {$_.UserPrincipalName -match "ourcloudnetwork.co.uk"}
Foreach ($user in $users) {
$upn = $user.UserPrincipalName.Replace("ourcloudnetwork.co.uk","ourcloudnetwork.com")
Set-ADUser -Identity $user.SamAccountName -UserPrincipalName $upn
}
Use functions within a Foreach Loop
When dealing with larger scripts that contain more complex actions within a foreach loop, it may make more sense to move some of the actions into a function, then call that function from within the loop. Although my example does not do it justice, the concept is still the same for tasks that are more complex.
$olddomain = "ourcloudnetwork.com"
$newdomain = "ourcloudnetwork.co.uk"
function Change-Upn {
$upn = $user.UserPrincipalName.Replace("$olddomain","$newdomain")
Set-ADUser -Identity $user.SamAccountName -UserPrincipalName $upn
}
$users = Get-ADUser -Filter * | Where {$_.UserPrincipalName -match "$olddomain"}
Foreach ($user in $users) {
Change-Upn
}
Build arrays with a foreach loop
Something that I find myself doing almost every time I use a foreach loop, is building some form of an array. Arrays can be built quickly and efficiently using foreach loops. First start by creating an empty array, before your foreach loop and then use the += operator to add a new item to your array.
Below is a simple example where we loop through each item stored in $items, then loop through each item and add the item property to an array.
$items = "1","2","3","4","5"
$array = @()
Foreach ($item in $items) {
$array += $item.property
}
Build reports with a foreach loop
Building reports with custom PowerShell objects is incredibly useful skill. Although you can often find all the information you need using relevant GET commands, building a report with a foreach loop will allow you to query additional information about each item in a collection and build a detailed report exactly to your required information
In the below example I am querying all users in Active Directory, then I am using that information to loop through each user and query their Exchange mailbox with the Get-Mailbox command.
$users = Get-ADUser -Filter *
$Report = [System.Collections.Generic.List[Object]]::new()
forEach ($user in $users) {
$Aliases = $null
$mailbox = $null
$mailbox = Get-Mailbox -Identity $user.UserPrincipalName
$Aliases = ($mailbox.EmailAddresses | ForEach-Object {$_ -replace "smtp:",""}) -join ","
$obj = [PSCustomObject][ordered]@{
"DisplayName" = $cuser.DisplayName
"UserPrincipalName" = $cuser.UserPrincipalName
"Aliases" = $Aliases
}
$report.Add($obj)
}
You can see from the code I first create a Collection object with:
$Report = [System.Collections.Generic.List[Object]]::new()
Then I build a custom PowerShell object which gets added to the Collection with the following code:
$report.Add($obj)
Create nested foreach loops
You can also nest foreach statements within an existing foreach statement. In the below example I am building a report about email groups in Exchange. I loop through each group, then I get the group members of each group and loop through each member to extract specific information and build a basic report.
$Report = [System.Collections.Generic.List[Object]]::new()
$GroupList = Get-DistributionGroup
Foreach ($group in $grouplist) {
$groupmembers = $null
$groupmembers = Get-DistributionGroupMember -Identity $group.Name
Foreach ($member in $groupmembers) {
$obj = [PSCustomObject][Ordered]@{
"Member" = $member.PrimarySmtpAddress
"Member Type" = $Member.RecipientType
"Group" = $Group.Name
"Group Address" = $Group.PrimarySmtpAddress
}
$Report.Add($obj)
}
}
Integrate IF statements within a foreach loop
Similar to nesting foreach statements, you can also nest or integrate IF statements within your loops to perform specific actions based on a criteria or to provide additional error checking in your scripts. In a very rudimentary way, we can implement an IF statement in our previous example to ensure we capture any groups in our report that do not contain any members.
$Report = [System.Collections.Generic.List[Object]]::new()
$GroupList = Get-DistributionGroup
Foreach ($group in $grouplist) {
$groupmembers = $null
$groupmembers = Get-DistributionGroupMember -Identity $group.Name
If ($groupmembers -eq "") {
$obj = [PSCustomObject][Ordered]@{
"Member" = "No members"
"Member Type" = "N/A"
"Group" = $Group.Name
"Group Address" = $Group.PrimarySmtpAddress
}
$Report.Add($obj)
} else {
Foreach ($member in $groupmembers) {
$obj = [PSCustomObject][Ordered]@{
"Member" = $member.PrimarySmtpAddress
"Member Type" = $Member.RecipientType
"Group" = $Group.Name
"Group Address" = $Group.PrimarySmtpAddress
}
$Report.Add($obj)
}
}
Where in the last script, if the group contained no members, no information was added to the report, now we have captured empty groups by implementing an IF statement at the start of our loop.