NUTS AND BOLTS PowerShell Loops Lead image: Lead Image © bowie15, 123RF.com
Lead Image © bowie15, 123RF.com
 

Features of PowerShell loops

Say Again?

Optimize your PowerShell scripts with loops when automating tasks that are repeated regularly and frequently. By Thomas Bär, Frank-Michael Schlede

Microsoft has emphasized that the increased use of the PowerShell scripting language will make life easier for system managers. After all, maintaining and operating Windows systems means above all that many tasks need to be handled regularly and for a large number of computers. Just as robots tirelessly fit doors and windshields on assembly lines, small and not-so-small scripts can handle recurring work, such as creating users, monitoring processes, or installing software.

Any programming or scripting language needs instructions the user can employ to design loops. It is irrelevant whether the language comes from the structured days of old or is one of the latest generation of object-oriented languages. PowerShell would not be PowerShell if it did not offer several approaches and options for almost every task and requirement – including loop constructs. Beginners thus first need to struggle with the distinction between the for loop, the foreach construct, and the foreach-object cmdlet – not forgetting the do and while loops, which also can be interrupted as needed.

The for Loop

Anyone who has ever programmed a computer is probably familiar with for loops. Using this construct lets arbitrary commands reiterate within a script block, as long as a previously defined condition is true. The basic structure looks like this:

for(<start value>; <run condition>; <iteration>)
{commands}

Use of this kind of loop is especially useful when the programmer knows exactly how often the loop should iterate. The following example creates exactly four text files with a corresponding index:

for($count=1; $count -lt 5; $count++)
  {New-Item -Path h:\tmp -name "file$count.txt" -itemtype file}

The variable $count is initially assigned the value 1; then, a file named file1.txt is created in the h:\tmp directory with the help of the New-Item cmdlet; then, $count is incremented by one with $count++. The loop is executed as long as the value for $count remains less than five (-lt 5).

Of course, you could also achieve exactly the same result with the race condition -le 4 (less than or equal to four): In both cases, the for loop iterates four times. Omitting individual components of the for loop can create infinite loops. The parentheses after for must contain at least two semicolons, as in

$count=1
For(;;) {$count}

or:

$count=2;
For(;;count+=2) {$count};

Whereas the first call constantly returns 1, the second call outputs even numbers starting with 2. To drop out of these loops, you need to press the Ctrl+C keyboard shortcut. Even though this type of call initially might not look very useful, you will find such constructs time and time again in applications using shell scripts.

We found a special tip in the PowerShell community for programming for loops that can prove very useful in practice. As our examples show here, loops are used to generate data and store data in variables, which in principle is unlikely to be a problem, even for large numbers of variables, given the speed of modern computer systems. The following example creates an empty array named table, fills it with 15,000 random numbers between 2 and 10, and reports the number of items in the table:

$table = @()
for($count=1; $count -le 15000; $count++)
{$table += Get-Random -Minimum 2 -Maximum 10}
$table.count

If you run this script, you will notice a certain delay – even on a powerful PC – before you see the results, because each time the script iterates through the loop the results of the Get-Random cmdlet are stored in an array variable. You can speed this up as follows:

$table = for($count=1; $count -le 15000; $count++)
{Get-Random -Minimum 2 -Maximum 10}
$table.count

When you run this code, the output appears on the screen far faster than with the first approach. In this case, the value is not stored in a variable by Get-Random on each iteration; instead, PowerShell takes care of creating and managing the array. At the end, the script outputs the results, which are stored in $table after the loop terminates.

ForEach and ForEach-Object

In addition to the for loop, PowerShell users can rely on foreach or foreach-object for loop programming. The basic syntax of a ForEach loop is:

foreach (<variable in collection>) {<commands>}

A simple example shows the basic workings of a ForEach loop:

$names = "Hugo", "Emmy Lou", "William", "Frank", "Jane"
foreach ($count in $names)
{"$count =" + $count.length}

It lists the elements in a collection, which is often an array. We created a simple string array with five elements. The loop starts with the foreach keyword, followed by parentheses with three components: The first component is the variable, which the user specifically defined previously for this ForEach command. In the example, it is called $count, but of course you can choose the name freely, as long as you adhere to the PowerShell naming conventions.

This variable contains the current element during each loop iteration, so it would contain the value Hugo on the first iteration and Emmy Lou during the second iteration. The second component within the parentheses is the in keyword, which always has to be used in exactly this way. Finally, the third component is the collection of values to be processed: It is represented by the variable $names.

The statements within the parentheses are processed during each iteration of the loop, with the current value of the control variable (i.e., $count). This example composes a string that outputs the number of characters in the string in addition to the content of the variable. The listed collection does not necessarily have to be an array, as the following example illustrates very nicely:

foreach ($service in Get-Service)
{ $service.name + "can be stopped:    " + $service.canstop.tostring().toupper() }

In this example, we called the Get-Service cmdlet in parentheses as the third component. It returns a collection of objects, in which one object is issued for each service on the local system. The loop is set up so that a service object is assigned to the $service variable at each pass. Inside the loop, the foreach statement then uses this variable to read the name of the service by using the name property of the service object. At the same time, explanatory text and a colon are appended to the name.

In the next step, a property of the service object named canstop is read. This property returns a Boolean value indicating whether a service can be stopped once started. Finally, the foreach statement calls the tostring and toupper methods to convert this value into a string with uppercase characters.

The order in which the two methods are used is important: The Boolean value first must be converted to a string with ToString before the ToUpper method can be used, because ToUpper can only be applied to strings. If you don't want to see the output of the Boolean value, False or True, in capital letters on the screen, you can leave out these two methods.

PowerShell programming is always interesting when you leverage the powerful pipe mechanism and object orientation. Both approaches come to fruition when you use the ForEach-Object cmdlet instead of the ForEach loop. In the next example, we will be using the Where-Object cmdlet – or, in this case, its alias where:

Get-Service | where {$_.status -eq 'running'} | foreach-object {$_.name +     " can be stopped:    " + $_.canstop.tostring().toupper() }

With this script, too, the system services are shown on the screen (Figure 1). The Get-Service cmdlet pipes these to Where-Object (where), where a filter then finds objects that have the status "running." Where-Object then sends these results to a pipe, which is read by foreach-object.

The ForEach-Object cmdlet can process objects directly from the pipe, which makes it very quick, especially with large volumes of data.
Figure 1: The ForEach-Object cmdlet can process objects directly from the pipe, which makes it very quick, especially with large volumes of data.

The foreach-object has no code in parentheses – just a script block in curly brackets. If you choose this approach, you also do not need a separate run condition variable, as shown here, which is one distinction between the foreach-object cmdlet and the foreach statements previously presented. Instead, we used the fixed shell variable $_., with which we can then access the current value of each object in the pipe. The PowerShell ForEach-Object also provides an alias named foreach. Although this may be confusing in terms of the code, PowerShell has no problem distinguishing this alias from the "normal" foreach. Whenever a foreach appears in a pipe, you can be sure that it is an alias for ForEach-Object.

Another difference lies in the way PowerShell handles the two calls: When the shell processes a foreach statement, it first creates the complete collection of objects in memory before it sets out to iterate through the loop. Therefore, it can take quite some time before processing begins if you have large collections. However, when the shell iterates through a foreach-object cmdlet, each object is processed as soon as it moves through the pipe (i.e., real-time processing).

Eternal Loops: do and while

If you are an experienced programmer, you might be familiar with the terms "head-controlled" (or "count-controlled") and "foot-controlled" loops. Classic for loops that query a condition before entering the loop body are considered "head-controlled," whereas loop constructs that test a condition only after processing the instructions in the loop body are referred to as "foot-controlled." Thanks to this principle, it is guaranteed that a foot-controlled loop in the program sequence will run at least once, whatever happens.

PowerShell lets users use such loops with the do and while statements. Basically do and while are useful for loops in which the programmer does not know how often they will iterate in the course of the program. You can even use them to create infinite loops. The formal structure of a do-while loop looks like this:

do {
<commands>
} while (<condition>)

The following is a very simple example of using such a loop:

do {
$input= Read-Host "Please enter"
} while ($input -notlike "secret" )
Write-Host "correct"

This loop runs until the user enters the word secret; the -notlike operator performs a string comparison to do this (Figure 2). Of course, this example is far removed from a real password because the comparison is not case sensitive; however, it illustrates the principle of this kind of loop. In terms of the program logic with this type of loop, it makes sense to query the cancellation condition at the beginning of the loop. In such cases, you would omit the do keyword and get started with the while query:

$file = [system.io.file]::OpenText("H:\tmp\poem.txt")
 **
while (!($file.EndOfStream)) { $file.ReadLine() }
$file.close
A loop that is processed at least once in the course of the program, the do-while loop only checks the run condition at the end of the loop body.
Figure 2: A loop that is processed at least once in the course of the program, the do-while loop only checks the run condition at the end of the loop body.

This little program first opens a text file. The while condition tests whether the end-of-file ($file.EndOfStream) has been reached; the query is negated by the ! character, which stands for "not." If the end-of-file is reached, we exit the loop and use $file.close to close the file outside of the loop.

Loop Exit: The continue and break Statements

In some scripts it can be useful, and sometimes necessary in terms of the program logic, to break out of the loop and its execution at some point. PowerShell provides for this scenario with the continue and break instructions. The following example shows their use in a foreach loop:

foreach ($file in Dir $env:windir) {
  if ($file -isnot [System.IO.FileInfo]) { continue }
  "@File {0} has a size of {1} bytes." -f $file.name,$file.length
}

This loop iterates through the Windows system directory (usually C:\Windows) and uses an if condition to test whether the object given to it is a file (Figure 3). If not, the continue statement is executed and the next iteration is triggered. Program execution jumps back to the loop head and reads the next object. To completely exit a loop, you can use the break statement. We can use the while loop example shown earlier; however, we have changed it to a genuine infinite loop by starting with the while ($true) condition (which is always true):

while ($true) {
$input= Read-Host "Please enter"
if ($input -eq "secret") {
Write-Host "exactly"
break }
else{
Write-Host "wrong" }
}
The continue statement jumps back to the head of the loop – so that subsequent statements are not executed – and thus to the next iteration, provided the if condition is true.
Figure 3: The continue statement jumps back to the head of the loop – so that subsequent statements are not executed – and thus to the next iteration, provided the if condition is true.

After prompting and getting the input, the secret password is queried again using an if statement and then compared using the -eq operator. If it is the same, the script returns the corresponding message and breaks out of the loop, thanks to the break statement. If it is not true, the else branch with the negative message is traversed and the loop starts again from the beginning.

Conclusions

PowerShell loops can simplify automation significantly in IT operations with very little use of resources. In this article, we have given you an overview of the various loop constructs and statements in PowerShell.