In my previous article in the Practical PowerShell series, I discussed looping as part of flow control. In this article, I cover another essential part of flow control: branching. Branching defines multiple paths for your code to follow, depending on conditions.
If-Then-Else
Next to loop constructs, there are decision-making cmdlets to control branching in code. It is an essential construct and allows you to branch code based on conditions (if) that are met (then) and optionally are not met (else). This construct is easy to understand and use because it follows natural language. You also see it in other environments, such as home automation or PowerAutomate. In the example below, we will process mailboxes disabling the accounts of Shared Mailboxes that are not already disabled:
$Mailboxes= Get-ExoMailbox -ResultSize Unlimited -Properties AccountDisabled ForEach( $Mailbox in $Mailboxes) { If( $Mailbox.RecipientTypeDetails -ieq 'SharedMailbox') { If(-not $Mailbox.AccountDisabled) { Write-Host( 'Disabling account of shared mailbox {0}' -f $Mailbox.userPrincipalName) } Else { Write-Host( 'Account of shared mailbox {0} already disabled' -f $Mailbox.userPrincipalName) } } Else { Write-Host ('{0} is not a shared mailbox' -f $Mailbox.userPrincipalName) } }
Note that the comparison operator ieq stands for case-insensitive equal. This is the default mode for eq, but I habitually mention it for clarity.
As shown in the example, you can nest If cmdlets, and the Else part is optional but often added as a placeholder with a descriptive comment for reference. When conditions get complex, keeping an overview becomes a challenge, and you might mismatch brackets or quotes. This is where editors such as Visual Studio Code or even the PowerShell ISE are helpful, indicating where the (current) matching bracket is when hovering over one, making missing opening or closing brackets or quotes easier to spot. Visual Studio Code also allows you to fold (or expand) the if or else branch, as shown in Figure 1.
Switch
As you saw in the If-Then-Else example, code can become quite complex and also unreadable when using multiple conditions or nesting. This is where the Switch construct comes in handy. The Switch construct allows you to turn this:
If( $Mailbox.recipientTypeDetails -ieq 'UserMailbox') { # code for UserMailbox ElseIf( $Mailbox.recipientTypeDetails -ieq 'SharedMailbox') { # code for SharedMailbox Else { # code for not UserMailbox nor SharedMailbox }
Into this:
Switch( $Mailbox.recipientTypeDetails) { 'UserMailbox' { # code for UserMailbox } 'SharedMailbox' { # code for SharedMailbox } default { # code for not UserMailbox nor SharedMailbox Write-Warning ('Unsupported type: {0}' -f $_) } }
The $Mailbox.recipientTypeDetails value is checked against ‘UserMailbox’ and ‘SharedMailbox’. When a match is found, the corresponding script block is run. If no match is found, the optional default code is executed. The variable $_ is assigned the value to match within the switch, which we use to report that we’ve encountered an unsupported value for the recipientTypeDetails property.
Switch also has some operation modifiers, such as the switches -regex and -wildcard. These allow Switch to check the evaluated object against a regular expression or a wildcard (similar to -like). Be advised that in principle every condition will get evaluated, which could result in more than one script block getting executed. For example, in the following segment showing Switch in wildcard mode, both the code for UserMailbox and *Mailbox will run when recipientTypeDetails is UserMailbox:
Switch -wildcard ( $Mailbox.recipientTypeDetails) { 'UserMailbox' { # code for UserMailbox } '*Mailbox' { # code for any type ending in Mailbox } }
This can be avoided by adding break at the end of code blocks, preventing further evaluation, e.g.
Switch -wildcard ( $Mailbox.recipientTypeDetails) { 'UserMailbox' { # code for UserMailbox break } '*Mailbox' { # code for any type ending in Mailbox break } }
The Switch cmdlet is powerful and can prevent code fromending up in overly complex if-then-else branching while keeping things readable. It also allows you to match on multiple values, for example. For more information on switch and additional usage, see here.
Stop Cmdlets
So far, we have seen constructs that execute code in a sequential order. There might be situations when you want to cut branching short or skip an iteration. For example, when you are looping through elements to locate a matching item, there is no need to continue the remainder of the loop when you find one. This is when Break can be used. On the other hand, if you do not want to process a certain element in the loop, you can use Continue.
Like Break, stop cmdlets are also available at the level of functions and scripts, in the form of Exit and Return. Exit terminates the function or script in the scope of where the function or script was called. Return exits the script or function, optionally returning an object to the scope of where it was called, after which execution will be continued. To understand the different effects of these cmdlets, have a look at the following example:
Function Test { For( $i=0; $i -lt 4; $i++) { Write-Host ('Start {0}' -f $i) If( $i -eq 2) { # Aborts the loop Break # Skips iteration, continues with next iteration Continue # Aborts execution, continues at caller scope Return # Exits and at scope of caller Exit } Write-Host ('End {0}' -f $i) } Write-Host ('End Function.') } Test Write-Host ('End Script.')
The effect of using each stopping cmdlet can be demonstrated by commenting on the ones you are not testing in the script above. The output for each uncommented stopping cmdlet is shown in the table below.
Break | Continue | Return | Exit |
Start 0 End 0 Start 1 End 1 Start 2 End Function. End Script. | Start 0 End 0 Start 1 End 1 Start 2 Start 3 End 3 End Function. End Script. | Start 0 End 0 Start 1 End 1 Start 2 End Script. | Start 0 End 0 Start 1 End 1 Start 2 |
Now after all that, I highly recommend avoiding stopping cmdlets and instead using a single point of exit for your loop or function. Those in favor of using stopping cmdlets usually claim they increase efficiency and performance. While that might be true for unstructured code, when you properly structure code, there is no real need to add stopping cmdlets. In addition, stopping cmdlets makes code harder to read and understand. Many organizations use code style guides that make single exit points compulsory.
Unary Operators
In some of the examples, I use unary operators. Unary operators let you write $i++ instead of $i=$i+1. You can also add an unary operator before the variable is evaluated by writing ++$i, in which case the 1 gets added to $i before it gets evaluated. You can use ‘–‘ for subtractions, e.g., $i–. You can also use unary operators to perform other arithmetic operations, e.g. $i+=2 will add 2 to $i, and $i*=4 will multiply the value stored in $i by 4. For more on unary operators, see here.
A common usage of unary operators is with string concatenation or when “adding” items to an array or object collection, e.g., $string+= $field or $array+= $item. However, behind the scenes, this code is executed as $array= $array + $item, which makes a copy of the $array object and adds $item. This might not only be slow for larger complex objects. It also explains why this works for arrays, which, by definition, have a fixed number of elements (and why I mentioned “adding” using quotes).
Object types such as strings or arrays have better alternative (.NET) object types that are more suited for intensive data manipulations, such as [System.Collections.ArrayList] instead of Array, or [System.Text.StringBuilder] for strings. Here is a quick comparison so that you get an idea:
# Add 10.000 numbers to an array PS> $Array= @() PS> Measure-Command { 1..10000 | ForEach{ $Array+= $_ } } | Select TotalMilliseconds TotalMilliseconds ----------------- 21746.56 # Add 10.000 numbers to arraylist PS> $ArrayList= [System.Collections.ArrayList]::new() PS> Measure-Command { 1..10000 | ForEach{ $ArrayList.Add( $_)| Out-Null } } | Select TotalMilliseconds TotalMilliseconds ----------------- 77.69
Running the above examples, you will notice that ArrayList is significantly faster than using Array when using its built-in Add method. Note that ArrayList’s Add returns the number of the element just added, so I suppress that in the example by piping output to Out-Null. Now, I don’t say you need to change all your code, but for scripts chewing away on larger data sets, the performance benefit is considerable.
Branching for Dynamic Execution
Like looping, branching is one of the elementary programming constructs. It adds dynamics to your code, letting your code make decisions on what to execute based on conditions. Also, getting familiar with the switch cmdlet will help when nested if-then-else statements become too complex, and lets you maintain readability and maintainability.
Feel free to reach out in the comments if you have questions or comments. If not, until the next article!
Very helpful, thanks
Had no idea about the Net object types having better performance. Will make my scripts much faster