The PowerShell for loop

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

The PowerShell for loop is similar to the for loop in other programming languages and it's usually used as a counter of some sort for processing items in an array/collection. More commonly used are the foreach loop and the ForEach-Object cmdlet. Click here to read about foreach loops and ForEach-Object.

In PowerShell you normally don't need for loops too often, but they're occasionally necessary - for instance when you need to keep track of an element's position in a collection - although you could always use foreach and a range of numbers to create the counter (x..y; e.g.: 0..9, for numbers from 0 to 9). Foreach and a range will normally be a lot faster.

Caching the array count will also speed it up significantly if it's repeated a lot. There's more on that here



The PowerShell for Loop's Syntax

The syntax is displayed in the following diagram:

For-loop-syntax.jpg

First, the initialization is run, then the condition is evaluated and while it's true the statements in the specified block will run. After the first run, the increment pipeline is processed/run and the condition is checked again. If it still evaluates to true, the code block runs again, the increment pipeline is run, and this process is repeated until the condition evaluates to a false value. If it never becomes false, you have an infinite loop - unless you use the break keyword inside, in conjunction with an if statement, or some such.

One place where PowerShell for loops differ from those seen in other languages, is that the initialization, condition and increment parts are actually pipelines. However, output from the pipelines is discarded, so you are left only with the side-effects.

Examples

Here are some examples.

The Typical for Loop Example

Something like this is the canonical example seen in most languages:

PS C:\> for ($i = 0; $i -lt 4; $i++) { $i }
0
1
2
3
PS C:\>

However, you can do more in the three pipelines if you so desire. Personally, I don't consider it too useful in most cases.

In a script, you need to spread the for loop out over multiple lines, and indent with four spaces (recommended best practice in PowerShell). When you press the TAB key in PowerShell ISE, it will automatically insert four spaces (or however many are necessary to align with the above code). It'll look similar to the following screenshot.

For-loop-example2-formatting-indenting.png

This would, however, more conventionally be written similar to the following in PowerShell:

PS C:\> 1..4 | foreach { $_ * 100 }
100
200
300
400

Using A Subexpression To Initialize Two Values

Here's an example where I initialize two variables by using a subexpression as the initialization pipeline. I set the array to the numbers 1-10 first, then it gets emptied in the for loop's initialization pipeline, which I verify after the for loop.

PS C:\> $CoolNums = 1..10
PS C:\> for ( $($CoolNums = @(); $i = 1); $i -le 4; $i++) { $CoolNums += $i }
PS C:\> $CoolNums
1
2
3
4
PS C:\>

Using The continue And break Keywords

This skips the numbers 3-5 using the continue statement which causes PowerShell to skip the remainder of the loop script block, and start on the next iteration (if you want to learn more about PowerShell regular expressions and the -match operator, click here). The more common approach would be numerical comparisons using the -ge, -gt, -lt or -le operators, or -Contains/-NotContains, but for the sake of this example, it's simply a lot shorter (-Contains will be about the same, when I think about it).

PS C:\> for ($i=0; $i -le 7; $i++) { if ($i -imatch '^(?:3|4|5)$') { continue }; $i }
0
1
2
6
7
PS C:\>

This breaks out of the loop when it hits the number 4 in the if statement.

PS C:\> for ($i=0; $i -le 7; $i++) { if ($i -eq 4) { break }; $i }
0
1
2
3
PS C:\>

A Way To Speed Up For Loops In A Common Scenario Where You Iterate An Array

Using my PowerShell benchmarking module I demonstrate how a simple caching of the array/collection count, rather than checking it once for each iteration, significantly speeds up the overall loop execution time, when you iterate said collection using a traditional for loop.

Doug Finke has a blog post about this caching technique here. I'm seeing even greater speed gains than the "4x" he is getting.

PS C:\> Import-Module Benchmark
PS C:\> $Array = 1..5000
PS C:\> Measure-These -Count 1000 -ScriptBlock { for ($i=0; $i -lt $Array.Count; ++$i) { [void] $null } },
{ $Count = $Array.Count; for ($i=0; $i -lt $Count; ++$i) { [void] $null } } -Title 'No caching', 'With caching' | ft -a

Title/no.    Average (ms) Count Sum (ms)     Maximum (ms) Minimum (ms)
---------    ------------ ----- --------     ------------ ------------
No caching   77.02013      1000 77,020.13140 84.62490     75.36490
With caching 12.63625      1000 12,636.25270 15.44690     11.74170


PS C:\>

As you can see, the caching is simply an assignment of the array's count to a variable, and then using that variable in the condition part of the loop later, rather than retrieving the .Count property once for each iteration:

$Count = $Array.Count

How It Compares To A foreach Loop and ForEach-Object

I decided to see how it compared to a foreach loop and the ForEach-Object cmdlet, in terms of merely iterating a collection of 5000 items. It turns out ForEach-Object has significant overhead compared to the foreach loop in this scenario. For some reason the foreach loop is also a lot faster than the for loop demonstrated above.

PS C:\> Measure-These -Count 1000 -ScriptBlock { 1..5000 | %{ [void] $null } },
{ foreach ($Temp in 1..5000) { [void] $null } } -Title 'Foreach-Object', 'foreach loop' | ft -AutoSize

Title/no.      Average (ms) Count Sum (ms)      Maximum (ms) Minimum (ms)
---------      ------------ ----- --------      ------------ ------------
Foreach-Object 318.65794     1000 318,657.94050 332.98630    308.71270
foreach loop   2.39288       1000 2,392.88410   3.84940      2.07470

Getting Freaky With It

For-loop-example.png