“Control, Control, You Must Learn Control.” I rarely quote Yoda’s wisdom in articles, but this is an exception. After discussing advanced functions in the second Practical PowerShell article, the other important scripting skill to master is using the flow control cmdlets. These cmdlets are not just theoretical concepts; they are practical assets that can significantly enhance your PowerShell scripts.

The concept of flow control is not new; it has been available in programming since the days of ENIAC (1945), where units of circuit boards with electric components were used to control the compute sequence.

These concepts still exist today in most programming languages and shell environments. However, every language has its way of managing flow, and PowerShell is no exception. In this article, I discuss the code flow constructs available in PowerShell regarding looping and how to use them. In the next article, I will dive into branching. Meanwhile, I might pass an additional tip or two, so read on.

Looping

You can use a loop to iterate through items of an array or a collection of objects, repeating code until a condition is satisfied or until the condition is no longer valid. You can iterate over items from arrays and collections until you have processed them all. In the examples for each loop option, I will use the example of calculating the average number of items in mailboxes.

For-Loop

For is the loop type that continues to execute code until the condition is met. To define the conditions, you can create an initialization statement, a condition to stop the code from getting executed, and a step. The step statement is run on every iteration after executing the code and usually increments a counter, making the loop terminate when finished.

An example of using For to go through an array of user objects:

$TotalItems= 0
[array]$Users = Get-User -RecipientTypeDetails UserMailbox
For( $i=0; $i -lt $Users.count; $i++) {
    $TotalItems+= (Get-ExoMailboxStatistics -Identity $Users[ $i].externalDirectoryObjectId).ItemCount
}
$Avg= $TotalItems / $Users.Count
Write-Host ('Avg. Itemcount: {0}' -f $Avg)

Note that [array] casts the result of running the Get-User cmdlet to an array. The result of Get-User can be a collection of user objects or a single user object. Casting the result prevents having to add code to anticipate handling these differences. For example, when Get-User results in a single object, you can directly refer to its properties, e.g. $User.Identity. However, when multiple objects arw returned, this creates an array of all identities of the returned user objects. Read more on casting here.

In the example, I use the -format operator, or -f for short. This allows you to remove variable references from the string, improving readability. To use a formatted string, use numbered markers starting with {0} in the text, pass the corresponding variable or text after the format operator in the corresponding position, where the first passed item becomes {0}, and so on. You can do much more with the format operator, such as number justifying. Read more on the format operator here.

ForEach-Object and ForEach

ForEach can refer to two different commands, which can be confusing. The first is ForEach as the alias for ForEach-Object (or %). It operates on pipeline input, going through the passed objects, which is usually an array or collection of objects. As ForEach-Object goes through every object, every object gets processed by a { code } script block, where the object itself can be referred to by the variable $_. You might already use ForEach-Object to process objects passed through a pipeline, especially when a cmdlet does not accept pipeline input or required parameters are not passed through the pipeline. For example:

$TotalItems= 0
$Users= 0
Get-User -RecipientTypeDetails UserMailbox | ForEach-Object {
    $TotalItems+= (Get-ExoMailboxStatistics -Identity $_.externalDirectoryObjectId).ItemCount
    $Users++
}
$Avg= $TotalItems / $Users
Write-Host ('Avg. Itemcount: {0}' -f $Avg)

Note that since we do not know the number of mailboxes upfront, we use an additional variable to keep track of the number to calculate the average.

The other ForEach is similar to For, and can be used to iterate over an enumerable object. In other words, every element of an array or object collection. You can define the variable name to become the current object as ForEach goes through the array or collection, for example:

$TotalItems= 0
$Users= Get-User -RecipientTypeDetails UserMailbox
ForEach( $User in $Users) {
    $TotalItems+= (Get-ExoMailboxStatistics -Identity $User.externalDirectoryObjectId).ItemCount
}
$Avg= $TotalItems / $Users.Count
Write-Host ('Avg. Itemcount: {0}' -f $Avg)

Note that I did not cast $Users to an array, as ForEach will properly handle the results regardless of whether the collection is single or multiple objects.

You can also use ForEach as a method of an array or collection, for example:

$TotalItems= 0
$Users= Get-User -RecipientTypeDetails UserMailbox $Users.ForEach{
    $TotalItems+= (Get-ExoMailboxStatistics -Identity $_.externalDirectoryObjectId).ItemCount
}
$Avg= $TotalItems / $Users.Count
Write-Host ('Avg. Itemcount: {0}' -f $Avg)

Apart from functional differences, the ForEach options can differ in performance. ForEach loads the complete collection prior to iteration, whereas ForEach-Object uses the pipeline to send objects through as they are fetched. The performance of the two commands depends on your collection and what processing takes place. If memory is a concern, ForEach-Object is the preferred choice. For readability, using the ForEach construct might be preferred.

ForEach-Object -Parallel

In PowerShell 7, the option exists to add a -Parallel switch to ForEach-Object, e.g.

$Users | ForEach-Object -Parallel {
    Get-ExoMailboxStatistics -Identity $_.id
}

While parallelization implies a performance benefit, remember you are using a shared service. Parallelization might result in the service throttling your code more quickly as it simultaneously fires requests at the Graph or one of the other workloads. In addition, the default for running in parallel is 5 (configurable using the ThrottleLimit parameter). Each parallel running piece of code runs in its own runspace, which is a separate context containing all of the functions and loaded modules of the caller. Just be advised that variables from the caller need to be referenced explicitly by prefixing them using $using, e.g.

$Props= 'TotalItemSize', 'TotalDeletedItemSize'
$Users | ForEach-Object -Parallel {
    Get-ExoMailboxStatistics -Identity $_.id -Properties $using:Props
}

A certain overhead is involved in creating and disposing of the runspaces running this code, which might harm overall performance if your code is more processing-oriented. If your code is talking to Microsoft 365, the overhead might be negligible compared to the additional overhead from just interacting with the service.

Finally, objects sent through the pipeline are normally processed in sequential order. When using -Parallel, the order is undetermined. So, using ForEach-Object with the Parallel switch might bring some benefits, but testing is advised as your mileage may vary.

While

A While loop is a conditional loop that processes the code when the condition is met and keeps repeating the code until the condition is no longer met. Depending on the condition, a while loop does not necessarily execute the code.

Practical 365 recently showed an example of using While in this example of how to fetch paginated results from Graph:

$Teams.Value.ForEach( {
   $TeamsHash.Add($_.Id, $_.DisplayName) } )
   $NextLink = $Teams.'@Odata.NextLink'
      While ($NextLink -ne $Null) {
      $Teams = Invoke-WebRequest -Method GET -Uri $NextLink -ContentType $ContentType -Headers $Headers | ConvertFrom-Json
      $Teams.Value.ForEach( {
         $TeamsHash.Add($_.Id, $_.DisplayName) } )
   $NextLink = $Teams.'@odata.NextLink'
}

Here, the value of the NextLink value returned by a Graph API request is retrieved, and if it contains a value (not null), the code will be iterated until no NextLink is remaining. A common pattern for While, as also visible in the example, is that the evaluation related to the condition stated before the while (to trigger the first run of code) and at the bottom of the loop script block, in this case $NextLink = $Teams.’@odata.NextLink’.

A word of warning. When working with loops such as While, ensure the condition is matched at some point to avoid infinite loops. The same applies to Do-While and Do-Until. For example, in the following code the incrementing of the index i is accidentally omitted, resulting in the loop never to terminate:

[array]$Users = Get-User -RecipientTypeDetails UserMailbox
$i=0
While( $i -lt $Users.count) {
    Get-ExoMailboxStatistics -Identity $Users[ $i].externalDirectoryObjectId).ItemCount
}

A Do-While is a conditional loop that runs code at least once, as opposed to While, and will continue repeating code as long as the condition is met. It is used when you are certain the code can execute. For this example, let us assume you create a guest user using the PowerShell Graph SDK and need to show a visual clue that the script is waiting for the provisioning to finish before it can modify certain Exchange Online-related properties, e.g.

$Alias= 'philip_contoso.com#ext'
$UPN= '{0}@contoso.onmicrosoft.com' -f $Alias
$Props= @{
  DisplayName= 'Philip'
  Mail= 'philip@contoso.com'
  MailNickname= $Alias
  UserPrincipalName= $UPN
  UserType= 'Guest'
}
$Account = New-MgUser @Props

Do {
  Write-Host ('Waiting for Guest provisioning ..')
  Start-Sleep 60
} While ( $null -eq (Get-MailUser -Identity $UPN -ErrorAction SilentlyContinue))

Set-MailUser -Identity $UPN -HiddenFromAddressListEnabled:$false

Do-Until

A Do-Until is a conditional loop that will loop and run code at least once and will continue repeating it until the condition is met. It differs from Do-While in that Do-While loops the code as long as the condition is met. Both can be refactored as the other, making it in my opinion a matter of trying to avoid double negatives and improved readability.

An example of a double negative could be something such as:

Do { .. } Until ( -not ( $number -lt 10))

This will loop the code until the number is not less than 10.

This could be rewritten as:

Do { .. } Until ( $number -ge 10)

or

Do { .. } While ( $number -lt 10)

Loops for Iteration

Talking about flow control touches on elementary programming. Looping is part of this. Knowing about these constructs and when which programming is appropriate helps you iterate through user or mailbox objects in a way that is efficient while retaining readability.

Feel free to reach out in the comments if you have questions or comments. If not, until the next article, where I will talk about another type of construct in flow control, which is branching.

About the Author

Michel De Rooij

Michel de Rooij is a consultant and Microsoft MVP since 2013. Michel lives in The Netherlands, and has been working in the IT industry for over 25 years. Starting out as a developer, Michel nowadays assists customers in their journey to or usage of Microsoft 365. His main focus is on Exchange On-Premises & Exchange Online, including related technologies such as (Azure) Active Directory. He loves to automate processes and procedures using platforms such as PowerShell. Michel is also active online in communities such as Microsoft's TechCommunity, or on socials networks such as Twitter (@mderooij). Michel runs a blog at eightwone.com, is a guest author for several other websites, a published author, and occasional speaker at international events.

Leave a Reply