Chapter 8. Loops

by Mar 16, 2012

Loops repeat PowerShell code and are the heart of automation. In this chapter, you will learn the PowerShell loop constructs.

Topics Covered:

ForEach-Object

Many PowerShell cmdlets return more than one result object. You can use a Pipeline loop: foreach-object to process them all one after another.. In fact, you can easily use this loop to repeat the code multiple times. The next line will launch 10 instances of the Notepad editor:

1..10 | Foreach-Object { notepad }


Foreach-Object is simply a cmdlet, and the script block following it really is an argument assigned to Foreach-Object:

1..10 | Foreach-Object -process { notepad }


Inside of the script block, you can execute any code. You can also execute multiple lines of code. You can use a semicolon to separate statements from each other in one line:

1..10 | Foreach-Object { notepad; "Launching Notepad!" }


In PowerShell editor, you can use multiple lines:

1..10 | Foreach-Object { notepad "Launching Notepad!" }


The element processed by the script block is available in the special variable $_:

1..10 | Foreach-Object { "Executing $_. Time" }


Most of the time, you will not feed numbers into Foreach-Object, but instead the results of another cmdlet. Have a look:

Get-Process | Foreach-Object { 'Process {0} consumes {1} seconds CPU time' -f $_.Name, $_.CPU }


Invoking Methods

Because ForEach-Object will give you access to each object in a pipeline, you can invoke methods of these objects. In Chapter 7, you learned how to take advantage of this to close all instances of the Notepad. This will give you much more control. You could use Stop-Process to stop a process. But if you want to close programs gracefully, you should provide the user with the opportunity to save unsaved work by also invoking the method CloseMainWindow(). The next line closes all instances of Notepad windows. If there is unsaved data, a dialog appears asking the user to save it first:

Get-Process notepad | ForEach-Object { $_.CloseMainWindow() }


You can also solve more advanced problems. If you want to close only those instances of Notepad that were running for more than 10 minutes, you can take advantage of the property StartTime. All you needed to do is calculate the cut-off date using New-Timespan. Let's first get a listing that tells you how many minutes an instance of Notepad has been running:

Get-Process notepad | ForEach-Object { 
  $info = $_ | Select-Object Name, StartTime, CPU, Minutes 
  $info.Minutes = New-Timespan $_.StartTime | Select-Object -expandproperty TotalMinutes
  $info
}

Check out a little trick. In the above code, the script block creates a copy of the incoming object using Select-Object, which selects the columns you want to view. We specified an additional property called Minutes to display the running minutes, which are not part of the original object. Select-Object will happily add that new property to the object. Next, we can fill in the information into the Minutes property. This is done using New-Timespan, which calculates the time difference between now and the time found in StartTime. Don't forget to output the $info object at the end or the script block will have no result.

To kill only those instances of Notepad that were running for more than 10 minutes, you will need a condition:

Get-Process Notepad | Foreach-Object {
  $cutoff = ( (Get-Date) - (New-Timespan -minutes 10) )
  if ($_.StartTime -lt $cutoff) { $_ }
}

This code would only return Notepad processes running for more than 10 minutes and you could pipe the result into Stop-Process to kill those.

What you see here is a Foreach-Object loop with an If condition. This is exactly what Where-Object does so if you need loops with conditions to filter out unwanted objects, you can simplify:

Get-Process Notepad | Where-Object {
  $cutoff = ( (Get-Date) - (New-Timespan -minutes 10) )
  $_.StartTime -lt $cutoff
}

Foreach

There is another looping construct called Foreach. Don't confuse this with the Foreach alias, which represents Foreach-Object. So, if you see a Foreach statement inside a pipeline, this really is a Foreach-Object cmdlet. The true Foreach loop is never used inside the pipeline. Instead, it can only live inside a code block.

While Foreach-Object obtains its entries from the pipeline, the Foreach statement iterates over a collection of objects:

# ForEach-Object lists each element in a pipeline:
Dir C: | ForEach-Object { $_.name }

# Foreach loop lists each element in a colection:
foreach ($element in Dir C:) { $element.name }


The true Foreach statement does not use the pipeline architecture. This is the most important difference because it has very practical consequences. The pipeline has a very low memory footprint because there is always only one object travelling the pipeline. In addition, the pipeline processes objects in real time. That's why it is safe to process even large sets of objects. The following line iterates through all files and folders on drive c:. Note how results are returned immediately:

Dir C: -recurse -erroraction SilentlyContinue | ForEach-Object { $_.FullName }


If you tried the same with foreach, the first thing you will notice is that there is no output for a long time. Foreach does not work in real time. So, it first collects all results before it starts to iterate. If you tried to enumerate all files and folders on your drive c:, chances are that your system runs out of memory before it has a chance to process the results. You must be careful with the following statement:

# careful!
foreach ($element in Dir C: -recurse -erroraction SilentlyContinue) { $element.FullName }


On the other hand, foreach is much faster than foreach-object because the pipeline has a significant overhead. It is up to you to decide whether you need memory efficient real-time processing or fast overall performance:

Measure-Command { 1..10000 | Foreach-Object { $_ } } | Select-Object -expandproperty TotalSeconds
0,9279656
Measure-Command { foreach ($element in (1..10000)) { $element } } | Select-Object -expandproperty TotalSeconds
0,0391117


Do and While

Do and While generate endless loops. Endless loops are a good idea if you don't know exactly how many times the loop should iterate. You must set additional abort conditions to prevent an endless loop to really run endlessly. The loop will end when the conditions are met.

Continuation and Abort Conditions

A typical example of an endless loop is a user query that you want to iterate until the user gives a valid answer. How long that lasts and how often the query will iterate depends on the user and his ability to grasp what you want.

do {
  $Input = Read-Host "Your homepage"
} while (!($Input -like "www.*.*"))

This loop asks the user for his home page Web address. While is the criteria that has to be met at the end of the loop so that the loop can be iterated once again. In the example, -like is used to verify whether the input matches the www.*.* pattern. While that's only an approximate verification, it usually suffices. You could also use regular expressions to refine your verification. Both procedures will be explained in detail in Chapter 13.

This loop is supposed to re-iterate only if the input is false. That's why "!" is used to simply invert the result of the condition. The loop will then be iterated until the input does not match a Web address.

In this type of endless loop, verification of the loop criteria doesn't take place until the end. The loop will go through its iteration at least once because you have to query the user at least once before you can check the criteria.

There are also cases in which the criteria needs to be verified at the beginning and not at the end of the loop. An example would be a text file that you want to read one line at a time. The file could be empty and the loop should check before its first iteration whether there's anything at all to read. To accomplish this, just put the While statement and its criteria at the beginning of the loop (and leave out Do, which is no longer of any use):

# Open a file for reading:
$file = [system.io.file]::OpenText("C:autoexec.bat")

# Continue loop until the end of the file has been reached:
while (!($file.EndOfStream)) { 

  # Read and output current line from the file:
  $file.ReadLine() 
}

# Close file again:
$file.close

Using Variables as Continuation Criteria

The truth is that the continuation criteria after While works like a simple switch. If the expression is $true, then the loop will be iterated; if it is $false, then it won't. Conditions are therefore not mandatory, but simply provide the required $true or $false. You could just as well have presented the loop with a variable instead of a comparison operation, as long as the variable contained $true or $false.

do {
  $Input = Read-Host "Your Homepage"
  if ($Input like "www.*.*") {
    # Input correct, no further query:
    $furtherquery = $false
  } else {
    # Input incorrect, give explanation and query again:
    Write-Host Fore "Red" "Please give a valid web address."
    $furtherquery = $true
  }
} while ($furtherquery) 
Your Homepage: hjkh 
Please give a valid web address.
Your Homepage: www.powershell.com 

Endless Loops without Continuation Criteria

You can also omit continuation criteria and instead simply use the fixed value $true after While. The loop will then become a genuinely endless loop, which will never stop on its own. Of course, that makes sense only if you exit the loop in some other way. The break statement can be used for this:

while ($true) {
  $Input = Read-Host "Your homepage"
  if ($Input like "www.*.*") {
    # Input correct, no further query:
    break
  } else {
    # Input incorrect, give explanation and ask again:
    Write-Host Fore "Red" "Please give a valid web address."
  }
} 
Your homepage: hjkh 
Please give a valid web address.
Your homepage: www.powershell.com 

For

You can use the For loop if you know exactly how often you want to iterate a particular code segment. For loops are counting loops. You can specify the number at which the loop begins and at which number it will end to define the number of iterations, as well as which increments will be used for counting. The following loop will output a sound at various 100ms frequencies (provided you have a soundcard and the speaker is turned on):

# Output frequencies from 1000Hz to 4000Hz in 300Hz increments
for ($frequency=1000 $frequency le 4000 $frequency +=300) {
  [System.Console]::Beep($frequency,100)
} 

For Loops: Just Special Types of the While Loop

If you take a closer look at the For loop, you'll quickly notice that it is actually only a specialized form of the While loop. The For loop, in contrast to the While loop, evaluates not only one, but three expressions:

  • Initialization: The first expression is evaluated when the loop begins.
  • Continuation criteria: The second expression is evaluated before every iteration. It basically corresponds to the continuation criteria of the While loop. If this expression is $true, the loop will iterate.
  • Increment: The third expression is likewise re-evaluated with every looping, but it is not responsible for iterating. Be careful as this expression cannot generate output.

These three expressions can be used to initialize a control variable, to verify whether a final value is achieved, and to change a control variable with a particular increment at every iteration of the loop. Of course, it is entirely up to you whether you want to use the For loop solely for this purpose.

A For loop can become a While loop if you ignore the first and the second expression and only use the second expression, the continuation criteria:

# First expression: simple While loop:
$i = 0 
while ($i lt 5) {
  $i++
  $i
} 
1
2
3
4
5

# Second expression: the For loop behaves like the While loop:
$i = 0 
for ($i -lt 5) {
  $i++
 $i
} 
1
2
3
4
5

Unusual Uses for the For Loop

Of course in this case, it might have been preferable to use the While loop right from the start. It certainly makes more sense not to ignore the other two expressions of the For loop, but to use them for other purposes. The first expression of the For loop can be generally used for initialization tasks. The third expression sets the increment of a control variable, as well as performs different tasks in the loop. In fact, you can also use it in the user query example we just reviewed:

for ($Input="" !($Input -like "www.*.*") $Input = Read-Host "Your homepage") {
  Write-Host -fore "Red" " Please give a valid web address."
}

In the first expression, the $input variable is set to an empty string. The second expression checks whether a valid Web address is in $input. If it is, it will use "!" to invert the result so that it is $true if an invalid Web address is in $input. In this case, the loop is iterated. In the third expression, the user is queried for a Web address. Nothing more needs to be in the loop. In the example, an explanatory text is output.

In addition, the line-by-line reading of a text file can be implemented by a For loop with less code:

for ($file = [system.io.file]::OpenText("C:autoexec.bat") !($file.EndOfStream) `
$line = $file.ReadLine()) { # Output read line: $line } $file.close() REM Dummy file for NTVDM

In this example, the first expression of the loop opened the file so it could be read. In the second expression, a check is made whether the end of the file has been reached. The "!" operator inverts the result again. It will return $true if the end of the file hasn't been reached yet so that the loop will iterate in this case. The third expression reads a line from the file. The read line is then output in the loop.

The third expression of the For loop is executed before every loop cycle. In the example, the current line from the text file is read. This third expression is always executed invisibly, which means you can't use it to output any text. So, the contents of the line are output within the loop.

Switch

Switch is not only a condition, but also functions like a loop. That makes Switch one of the most powerful statements in PowerShell. Switch works almost exactly like the Foreach loop. Moreover, it can evaluate conditions. For a quick demonstration, take a look at the following simple Foreach loop:

$array = 1..5 
foreach ($element in $array)
{
  "Current element: $element"
} 
Current element: 1
Current element: 2
Current element: 3
Current element: 4
Current element: 5

If you use switch, this loop would look like this:

$array = 1..5 
switch ($array)
{
  Default { "Current element: $_" }
} 
Current element: 1
Current element: 2
Current element: 3
Current element: 4
Current element: 5

The control variable that returns the current element of the array for every loop cycle cannot be named for Switch, as it can for Foreach, but is always called $_. The external part of the loop functions in exactly the same way. Inside the loop, there's an additional difference: while Foreach always executes the same code every time the loop cycles, Switch can utilize conditions to execute optionally different code for every loop. In the simplest case, the Switch loop contains only the default statement. The code that is to be executed follows it in curly brackets.

That means Foreach is the right choice if you want to execute exactly the same statements for every loop cycle. On the other hand, if you'd like to process each element of an array according to its contents, it would be preferable to use Switch:

$array = 1..5 
switch ($array)
{
  1  { "The number 1" }
  {$_ -lt 3}  { "$_ is less than 3" }
  {$_ % 2}  { "$_ is odd" }
  Default { "$_ is even" }
} 
The number 1
1 is less than 3
1 is odd
2 is less than 3
3 is odd
4 is even
5 is odd

If you're wondering why Switch returned this result, take a look at Chapter 7 where you'll find an explanation of how Switch evaluates conditions. What's important here is the other, loop-like aspect of Switch.

Exiting Loops Early

You can exit all loops by using the Break statement, which will give you the additional option of defining additional stop criteria in the loop. The following is a little example of how you can ask for a password and then use Break to exit the loop as soon as the password "secret" is entered.

while ($true) 
{
  $password = Read-Host "Enter password"
  if ($password -eq "secret") {break}
}

Continue: Skipping Loop Cycles

The Continue statement aborts the current loop cycle, but does continue the loop. The next example shows you how to abort processing folders and only focus on files returned by Dir:

foreach ($entry in Dir $env:windir) 
{
  # If the current element matches the desired type, continue immediately with the next element:
  if (!($entry -is [System.IO.FileInfo])) { continue }

  "File {0} is {1} bytes large." -f $entry.name, $entry.length
}

Of course, you can also use a condition to filter out sub-folders:

foreach ($entry in Dir $env:windir) 
{
  if ($entry -is [System.IO.FileInfo]) {
    "File {0} is {1} bytes large." -f $entry.name, $entry.length
  }
}

This also works in a pipeline using a Where-Object condition:

Dir $env:windir | Where-Object { $_ -is [System.IO.FileInfo] }

Nested Loops and Labels

Loops may be nested within each other. However, if you do nest loops, how do Break and Continue work? They will always affect the inner loop, which is the loop that they were called from. However, you can label loops and then submit the label to continue or break if you want to exit or skip outer loops, too.

The next example nests two Foreach loops. The first (outer) loop cycles through a field with three WMI class names. The second (inner) loop runs through all instances of the respective WMI class. This allows you to output all instances of all three WMI classes. The inner loop checks whether the name of the current instance begins with "a"; if not, the inner loop will then invoke Continue skip all instances not beginning with "a." The result is a list of all services, user accounts, and running processes that begin with "a":

foreach ($wmiclass in "Win32_Service","Win32_UserAccount","Win32_Process") 
{
  foreach ($instance in Get-WmiObject $wmiclass) {
    if (!(($instance.name.toLower()).StartsWith("a"))) {continue}
    "{0}: {1}" f $instance.__CLASS, $instance.name
  }
}
Win32_Service: AeLookupSvc
Win32_Service: AgereModemAudio
Win32_Service: ALG
Win32_Service: Appinfo
Win32_Service: AppMgmt
Win32_Service: Ati External Event Utility
Win32_Service: AudioEndpointBuilder
Win32_Service: Audiosrv
Win32_Service: Automatic LiveUpdate  Scheduler
Win32_UserAccount: Administrator
Win32_Process: Ati2evxx.exe
Win32_Process: audiodg.exe
Win32_Process: Ati2evxx.exe
Win32_Process: AppSvc32.exe
Win32_Process: agrsmsvc.exe
Win32_Process: ATSwpNav.exe

As expected, the Continue statement in the inner loop has had an effect on the inner loop where the statement was contained. But how would you change the code if you'd like to see only the first element of all services, user accounts, and processes that begins with "a"? Actually, you would do almost the exact same thing, except now Continue would need to have an effect on the outer loop. Once an element was found that begins with "a," the outer loop would continue with the next WMI class:

:WMIClasses foreach ($wmiclass in "Win32_Service","Win32_UserAccount","Win32_Process") {
  :ExamineClasses foreach ($instance in Get-WmiObject $wmiclass) {
    if (($instance.name.toLower()).StartsWith("a")) {
      "{0}: {1}" f $instance.__CLASS, $instance.name
      continue WMIClasses
    }
  }
}
Win32_Service: AeLookupSvc
Win32_UserAccount: Administrator
Win32_Process: Ati2evxx.exe

Summary

The cmdlet ForEach-Object will give you the option of processing single objects of the PowerShell pipeline, such as to output the data contained in object properties as text or to invoke methods of the object. Foreach is a similar type of loop whose contents do not come from the pipeline, but from an array or a collection.

In addition, there are endless loops that iterate a code block until a particular condition is met. The simplest type is While, which checks its continuation criteria at the beginning of the loop. If you want to do the checking at the end of the loop, choose Do…While. The For loop is an extended While loop, because it can count loop cycles and automatically terminate the loop after a designated number of iterations.

This means that For is best suited for loops which need to be counted or must complete a set number of iterations. On the other hand, Do…While and While are designed for loops that have to be iterated as long as the respective situation and running time conditions require it.

Finally, Switch is a combined Foreach loop with integrated conditions so that you can immediately implement different actions independently of the read element. Moreover, Switch can step through the contents of text files line-by-line and evaluate even log files of substantial size.

All loops can exit ahead of schedule with the help of Break and skip the current loop cycle with the help of Continue. In the case of nested loops, you can assign an unambiguous name to the loops and then use this name to apply Break or Continue to nested loops.