Chapter 4. Looping and Flow Control

Introduction

As you begin to write scripts or commands that interact with unknown data, the concepts of looping and flow control become increasingly important.

PowerShell’s looping statements and commands let you perform an operation (or set of operations) without having to repeat the commands themselves. This includes, for example, doing something a specified number of times, processing each item in a collection, or working until a certain condition comes to pass.

PowerShell’s flow control and comparison statements let you adapt your script or command to unknown data. They let you execute commands based on the value of that data, skip commands based on the value of that data, and more.

Together, looping and flow control statements add significant versatility to your PowerShell toolbox.

Make Decisions with Comparison and Logical Operators

Problem

You want to compare some data with other data and make a decision based on that comparison.

Solution

Use PowerShell’s logical operators to compare pieces of data and make decisions based on them.

Comparison operators

-eq, -ne, -ge, -gt, -in, -notin, -lt, -le, -like, -notlike, -match, -notmatch, -contains, -notcontains, -is, -isnot

Logical operators

-and, -or, -xor, -not

For a detailed description (and examples) of these operators, see Comparison Operators.

Discussion

PowerShell’s logical and comparison operators let you compare pieces of data or test data for some condition. An operator either compares two pieces of data (a binary operator) or tests one piece of data (a unary operator). All comparison operators are binary operators (they compare two pieces of data), as are most of the logical operators. The only unary logical operator is the -not operator, which returns the true/false opposite of the data that it tests.

Comparison operators compare two pieces of data and return a result that depends on the specific comparison operator. For example, you might want to check whether a collection has at least a certain number of elements:

PS > (dir).Count -ge 4
True

or check whether a string matches a given regular expression:

PS > "Hello World" -match "H.*World"
True

Most comparison operators also adapt to the type of their input. For example, when you apply them to simple data such as a string, the -like and -match comparison operators determine whether the string matches the specified pattern. When you apply them to a collection of simple data, those same comparison operators return all elements in that collection that match the pattern you provide.

Note

The -match operator takes a regular expression as its argument. One of the more common regular expression symbols is the $ character, which represents the end of line. The $ character also represents the start of a PowerShell variable, though! To prevent PowerShell from interpreting characters as language terms or escape sequences, place the string in single quotes rather than double quotes:

PS > "Hello World" -match "Hello"
True
PS > "Hello World" -match 'Hello$'
False

By default, PowerShell’s comparison operators are case-insensitive. To use the case-sensitive versions, prefix them with the character c:

-ceq, -cne, -cge, -cgt, -cin, -clt, -cle, -clike, -cnotlike, 
-cmatch, -cnotmatch, -ccontains, -cnotcontains

For a detailed description of the comparison operators, their case-sensitive counterparts, and how they adapt to their input, see Comparison Operators.

Logical operators combine true or false statements and return a result that depends on the specific logical operator. For example, you might want to check whether a string matches the wildcard pattern you supply and that it is longer than a certain number of characters:

PS > $data = "Hello World"
PS > ($data -like "*llo W*") -and ($data.Length -gt 10)
True
PS > ($data -like "*llo W*") -and ($data.Length -gt 20)
False

Some of the comparison operators actually incorporate aspects of the logical operators. Since using the opposite of a comparison (such as -like) is so common, PowerShell provides comparison operators (such as -notlike) that save you from having to use the -not operator explicitly.

For a detailed description of the individual logical operators, see Comparison Operators.

Comparison operators and logical operators (when combined with flow control statements) form the core of how we write a script or command that adapts to its data and input.

See also Conditional Statements for detailed information about these statements.

For more information about PowerShell’s operators, type Get-Help About_Operators.

Adjust Script Flow Using Conditional Statements

Problem

You want to control the conditions under which PowerShell executes commands or portions of your script.

Solution

Use PowerShell’s if, elseif, and else conditional statements to control the flow of execution in your script.

For example:

$temperature = 90

if($temperature -le 0)
{
   "Balmy Canadian Summer"
}
elseif($temperature -le 32)
{
   "Freezing"
}
elseif($temperature -le 50)
{
   "Cold"
}
elseif($temperature -le 70)
{
   "Warm"
}
else
{
   "Hot"
}

Discussion

Conditional statements include the following:

if statement

Executes the script block that follows it if its condition evaluates to true

elseif statement

Executes the script block that follows it if its condition evaluates to true and none of the conditions in the if or elseif statements before it evaluate to true

else statement

Executes the script block that follows it if none of the conditions in the if or elseif statements before it evaluate to true

In addition to being useful for script control flow, conditional statements are often a useful way to assign data to a variable. PowerShell makes this very easy by letting you assign the results of a conditional statement directly to a variable:

$result = if(Get-Process -Name notepad) { "Running" } else { "Not running" }

This technique is the equivalent of a ternary operator in other programming languages, or can form the basis of one if you’d like a more compact syntax.

For more information about these flow control statements, type Get-Help About_Flow_Control.

Manage Large Conditional Statements with Switches

Problem

You want to find an easier or more compact way to represent a large ifelseifelse conditional statement.

Solution

Use PowerShell’s switch statement to more easily represent a large ifelseifelse conditional statement.

For example:

$temperature = 20

switch($temperature)
{
   { $_ -lt 32 }   { "Below Freezing"; break }
   32              { "Exactly Freezing"; break }
   { $_ -le 50 }   { "Cold"; break }
   { $_ -le 70 }   { "Warm"; break }
   default         { "Hot" }
}

Discussion

PowerShell’s switch statement lets you easily test its input against a large number of comparisons. The switch statement supports several options that allow you to configure how PowerShell compares the input against the conditions—such as with a wildcard, regular expression, or even an arbitrary script block. Since scanning through the text in a file is such a common task, PowerShell’s switch statement supports that directly. These additions make PowerShell switch statements a great deal more powerful than those in C and C++.

As another example of the switch statement in action, consider how to determine the SKU of the current operating system. For example, is the script running on Windows 7 Ultimate? Windows Server Cluster Edition? The Get-CimInstance cmdlet lets you determine the operating system SKU, but unfortunately returns its result as a simple number. A switch statement lets you map these numbers to their English equivalents based on the official documentation listed at this site:

##############################################################################
##
## Get-OperatingSystemSku
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Gets the sku information for the current operating system

.EXAMPLE

PS > Get-OperatingSystemSku
Professional with Media Center

#>

param($Sku = 
    (Get-CimInstance Win32_OperatingSystem).OperatingSystemSku)

Set-StrictMode -Version 3

switch ($Sku)
{
    0   { "An unknown product"; break; }
    1   { "Ultimate"; break; }
    2   { "Home Basic"; break; }
    3   { "Home Premium"; break; }
    4   { "Enterprise"; break; }
    5   { "Home Basic N"; break; }
    6   { "Business"; break; }
    7   { "Server Standard"; break; }
    8   { "Server Datacenter (full installation)"; break; }
    9   { "Windows Small Business Server"; break; }
    10  { "Server Enterprise (full installation)"; break; }
    11  { "Starter"; break; }
    12  { "Server Datacenter (core installation)"; break; }
    13  { "Server Standard (core installation)"; break; }
    14  { "Server Enterprise (core installation)"; break; }
    15  { "Server Enterprise for Itanium-based Systems"; break; }
    16  { "Business N"; break; }
    17  { "Web Server (full installation)"; break; }
    18  { "HPC Edition"; break; }
    19  { "Windows Storage Server 2008 R2 Essentials"; break; }
    20  { "Storage Server Express"; break; }
    21  { "Storage Server Standard"; break; }
    22  { "Storage Server Workgroup"; break; }
    23  { "Storage Server Enterprise"; break; }
    24  { "Windows Server 2008 for Windows Essential Server Solutions"; break; }
    25  { "Small Business Server Premium"; break; }
    26  { "Home Premium N"; break; }
    27  { "Enterprise N"; break; }
    28  { "Ultimate N"; break; }
    29  { "Web Server (core installation)"; break; }
    30  { "Windows Essential Business Server Management Server"; break; }
    31  { "Windows Essential Business Server Security Server"; break; }
    32  { "Windows Essential Business Server Messaging Server"; break; }
    33  { "Server Foundation"; break; }
    34  { "Windows Home Server 2011"; break; }
    35  { "Windows Server 2008 without Hyper-V for Windows Essential Server
           Solutions"; break; }
    36  { "Server Standard without Hyper-V"; break; }
    37  { "Server Datacenter without Hyper-V (full installation)"; break; }
    38  { "Server Enterprise without Hyper-V (full installation)"; break; }
    39  { "Server Datacenter without Hyper-V (core installation)"; break; }
    40  { "Server Standard without Hyper-V (core installation)"; break; }
    41  { "Server Enterprise without Hyper-V (core installation)"; break; }
    42  { "Microsoft Hyper-V Server"; break; }
    43  { "Storage Server Express (core installation)"; break; }
    44  { "Storage Server Standard (core installation)"; break; }
    45  { "Storage Server Workgroup (core installation)"; break; }
    46  { "Storage Server Enterprise (core installation)"; break; }
    46  { "Storage Server Enterprise (core installation)"; break; }
    47  { "Starter N"; break; }
    48  { "Professional"; break; }
    49  { "Professional N"; break; }
    50  { "Windows Small Business Server 2011 Essentials"; break; }
    51  { "Server For SB Solutions"; break; }
    52  { "Server Solutions Premium"; break; }
    53  { "Server Solutions Premium (core installation)"; break; }
    54  { "Server For SB Solutions EM"; break; }
    55  { "Server For SB Solutions EM"; break; }
    56  { "Windows MultiPoint Server"; break; }
    59  { "Windows Essential Server Solution Management"; break; }
    60  { "Windows Essential Server Solution Additional"; break; }
    61  { "Windows Essential Server Solution Management SVC"; break; }
    62  { "Windows Essential Server Solution Additional SVC"; break; }
    63  { "Small Business Server Premium (core installation)"; break; }
    64  { "Server Hyper Core V"; break; }
    72  { "Server Enterprise (evaluation installation)"; break; }
    76  { "Windows MultiPoint Server Standard (full installation)"; break; }
    77  { "Windows MultiPoint Server Premium (full installation)"; break; }
    79  { "Server Standard (evaluation installation)"; break; }
    80  { "Server Datacenter (evaluation installation)"; break; }
    84  { "Enterprise N (evaluation installation)"; break; }
    95  { "Storage Server Workgroup (evaluation installation)"; break; }
    96  { "Storage Server Standard (evaluation installation)"; break; }
    98  { "Windows 8 N"; break; }
    99  { "Windows 8 China"; break; }
    100 { "Windows 8 Single Language"; break; }
    101 { "Windows 8"; break; }
    103 { "Professional with Media Center"; break; }

    default {"UNKNOWN: " + $SKU }
}

Although used as a way to express large conditional statements more cleanly, a switch statement operates much like a large sequence of if statements, as opposed to a large sequence of ifelseifelseifelse statements. Given the input that you provide, PowerShell evaluates that input against each of the comparisons in the switch statement. If the comparison evaluates to true, PowerShell then executes the script block that follows it. Unless that script block contains a break statement, PowerShell continues to evaluate the following comparisons.

For more information about PowerShell’s switch statement, see Conditional Statements or type Get-Help About_Switch.

Repeat Operations with Loops

Problem

You want to execute the same block of code more than once.

Solution

Use one of PowerShell’s looping statements (for, foreach, while, and do) or PowerShell’s Foreach-Object cmdlet to run a command or script block more than once. For a detailed description of these looping statements, see Looping Statements. For example:

for loop
for($counter = 1; $counter -le 10; $counter++)
{
  "Loop number $counter"
}
foreach loop
foreach($file in dir)
{
  "File length: " + $file.Length
}
Foreach-Object cmdlet
Get-ChildItem | Foreach-Object { "File length: " + $_.Length }
while loop
$response = ""
while($response -ne "QUIT")
{
  $response = Read-Host "Type something"
}
do..while loop
$response = ""
do
{
  $response = Read-Host "Type something"
} while($response -ne "QUIT")
do..until loop
$response = ""
do
{
  $response = Read-Host "Type something"
} until($response -eq "QUIT")

Discussion

Although any of the looping statements can be written to be functionally equivalent to any of the others, each lends itself to certain problems.

You usually use a for loop when you need to perform an operation an exact number of times. Because using it this way is so common, it is often called a counted for loop.

You usually use a foreach loop when you have a collection of objects and want to visit each item in that collection. If you do not yet have that entire collection in memory (as in the dir collection from the foreach example shown earlier), the Foreach-Object cmdlet is usually a more efficient alternative.

Unlike the foreach loop, the Foreach-Object cmdlet lets you process each element in the collection as PowerShell generates it. This is an important distinction; asking PowerShell to collect the entire output of a large command (such as Get-Content hugefile.txt) in a foreach loop can easily drag down your system.

Note

A handy shortcut to repeat an operation on the command line is:

PS > 1..10 | foreach { "Working" }
Working
Working
Working
Working
Working
Working
Working 
Working
Working
Working

Like pipeline-oriented functions, the Foreach-Object cmdlet lets you define commands to execute before the looping begins, during the looping, and after the looping completes:

PS > "a","b","c" | Foreach-Object `
    -Begin { "Starting"; $counter = 0 } `
    -Process { "Processing $_"; $counter++ } `
    -End { "Finishing: $counter" }

Starting
Processing a
Processing b
Processing c
Finishing: 3

The while and do..while loops are similar, in that they continue to execute the loop as long as its condition evaluates to true. A while loop checks for this before running your script block, whereas a do..while loop checks the condition after running your script block. A do..until loop is exactly like a do..while loop, except that it exits when its condition returns $true, rather than when its condition returns $false.

For a detailed description of these looping statements, see Looping Statements or type Get-Help About_For, Get-Help About_Foreach, Get-Help about_While, or Get-Help about_Do.

Add a Pause or Delay

Problem

You want to pause or delay your script or command.

Solution

To pause until the user presses the Enter key, use the pause command :

PS > pause
Press Enter to continue...:

To pause until the user presses any key, use the ReadKey() method on the $host object:

PS > $host.UI.RawUI.ReadKey()

To pause a script for a given amount of time, use the Start-Sleep cmdlet:

PS > Start-Sleep 5
PS > Start-Sleep -Milliseconds 300

Discussion

When you want to pause your script until the user presses a key or for a set amount of time, pause and Start-Sleep are the two cmdlets you are most likely to use.

Note

If you want to retrieve user input rather than just pause, the Read-Host cmdlet lets you read input from the user. For more information, see Read a Line of User Input.

In other situations, you may sometimes want to write a loop in your script that runs at a constant speed—such as once per minute or 30 times per second. That is typically a difficult task, as the commands in the loop might take up a significant amount of time, or even an inconsistent amount of time.

In the past, many computer games suffered from solving this problem incorrectly. To control their game speed, game developers added commands to slow down their game. For example, after much tweaking and fiddling, the developers might realize that the game plays correctly on a typical machine if they make the computer count to 1 million every time it updates the screen. Unfortunately, the speed of these commands (such as counting) depends heavily on the speed of the computer. Since a fast computer can count to 1 million much more quickly than a slow computer, the game ends up running much more quickly (often to the point of incomprehensibility) on faster computers!

To make your loop run at a regular speed, you can measure how long the commands in a loop take to complete, and then delay for whatever time is left, as shown in Example 4-1.

Example 4-1. Running a loop at a constant speed

$loopDelayMilliseconds = 650
while($true)
{
   $startTime = Get-Date

   ## Do commands here
   "Executing"

   $endTime = Get-Date
   $loopLength = ($endTime - $startTime).TotalMilliseconds
   $timeRemaining = $loopDelayMilliseconds - $loopLength

   if($timeRemaining -gt 0)
   {
      Start-Sleep -Milliseconds $timeRemaining
   }
}

For more information about the Start-Sleep cmdlet, type Get-Help Start-Sleep.

Get Windows PowerShell Cookbook, 3rd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.