Learn PowerShell - Loops & Iteration
Episode 6 of 31

Learn PowerShell - Loops & Iteration

Understanding loops for processing bulk data: the difference between foreach and ForEach-Object and the $_ automatic variable in the pipeline, for loops with a counter, while and do-until for conditions, Break and Continue control, plus performance considerations between pipeline and foreach.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 5 you built scripts that make decisions: if, switch, and friends. But decisions only matter if they're applied to lots of data. In the real world, you won't write commands one by one for a hundred files — you need to repeat. This episode covers loops: foreach, for, while, and do — the engines that process collections in bulk.

If if is a gate that selects a path, a loop is a conveyor belt that passes items one by one through that gate. Combine the two, and you have a script that checks, modifies, and filters hundreds of objects in an instant.

foreach and ForEach-Object

There are two syntaxes that look similar but behave differently. First, the foreach statement, which processes an entire collection:

foreach statement
foreach ($file in Get-ChildItem /data) {
    Write-Host "Memproses $($file.Name)"
}

Second, the ForEach-Object cmdlet used in the pipeline:

ForEach-Object in the pipeline
Get-ChildItem /data | ForEach-Object {
    Write-Host "Memproses $($_.Name)"
}

The difference is fundamental: foreach loads the whole collection into memory then iterates; ForEach-Object processes each item as it flows through the pipeline. For small collections you won't feel the difference, but for millions of items this difference decides.

The $_ Automatic Variable

Inside a ForEach-Object block, the $_ variable (or $PSItem) refers to the item currently being processed. This is an automatic variable populated by the pipeline — you don't need to (and shouldn't) set it yourself.

Using $_
Get-Service | ForEach-Object {
    Write-Host "$($_.Name) sedang $($_.Status)"
}

You also met $_ in the catch block in error handling contexts — there it points to the error record. Here it points to the pipeline object. Same concept: one automatic variable whose context is determined by its environment.

The -Begin, -Process, -End Blocks

ForEach-Object accepts three blocks at once: -Begin runs once before the first item, -Process runs for each item, and -End runs once after the last item.

-Begin -Process -End
Get-Process pwsh | ForEach-Object `
    -Begin { $total = 0 } `
    -Process { $total += $_.WS } `
    -End { Write-Host "Total memori: $total bytes" }

This pattern is useful for aggregation — calculating totals, collecting lists, or opening-closing resources. -Begin prepares the accumulator, -Process weighs it, -End reports the result.

The for Loop with a Counter

When the number of iterations is already known, use for — the classic loop with a counter:

for loop
for ($i = 0; $i -lt 5; $i++) {
    Write-Host "Iterasi ke-$i"
}

The three parts inside the parentheses: initialization $i = 0, condition $i -lt 5, and increment $i++. Execution order: check the condition, run the block, increment the counter, repeat. Great for processing arrays by index:

for with array index
$nama = @("Ayu", "Budi", "Citra")
for ($i = 0; $i -lt $nama.Count; $i++) {
    Write-Host "$($i + 1). $($nama[$i])"
}

The while Loop

while repeats as long as the condition is true, without knowing in advance when it will stop:

while loop
$antrian = 5
while ($antrian -gt 0) {
    Write-Host "Sisa antrian: $antrian"
    $antrian--
}

The condition is checked before the block runs. If the condition is already false at the start, the block never runs. while suits things with indefinite duration: waiting for a service to start, waiting for a file to appear, or polling status.

do-while and do-until

These two siblings of while reverse the order: the block runs first, the condition is checked after. This means the block is guaranteed to run at least once.

do-while
do {
    Write-Host "Mencoba koneksi..."
    Start-Sleep -Seconds 2
    $siap = Test-NetConnection -ComputerName server -Port 443 -InformationLevel Quiet
} while (-not $siap)

do-until is its mirror: it repeats until the condition becomes true.

do-until
$i = 0
do {
    $i++
} until ($i -ge 10)
Write-Host "Selesai di iterasi ke-$i"

Note the difference from while: do-while stops when the condition is false, do-until stops when the condition is true.

Break, Continue, Return

Three keywords control the flow inside loops:

  • breakexits the loop entirely.
  • continuejumps to the next iteration, skipping the rest of the block.
  • return — exits the function or script containing the loop.
break and continue
for ($i = 1; $i -le 10; $i++) {
    if ($i -eq 3) { continue }
    if ($i -eq 8) { break }
    Write-Host $i
}

The result: 1, 2, 4, 5, 6, 7. Number 3 is skipped by continue; when $i reaches 8, break stops the loop entirely.

Pipeline vs foreach and Performance

A common question: which is faster, foreach or ForEach-Object?

Simple comparison
$data = 1..100000
 
$start = Get-Date
foreach ($x in $data) { $null = $x }
"foreach: $( (Get-Date) - $start )"
 
$start = Get-Date
$data | ForEach-Object { $null = $_ }
"ForEach-Object: $( (Get-Date) - $start )"

In many cases foreach is faster because it doesn't go through pipeline overhead and doesn't create per-item objects. However ForEach-Object is memory-efficient because it processes streaming — it doesn't need to hold the entire collection in memory.

Tip

Rule of thumb: for small-to-medium collections already in variables, use foreach. For data flowing from another cmdlet in a pipeline, use ForEach-Object — and in episode 8 you'll learn -Parallel, which runs iterations concurrently in PowerShell 7.

Practical Exercise

Combine everything: a script that summarizes large files in a folder.

rangkum-file.ps1
$folder = "/data"
$batasMb = 100
 
$files = Get-ChildItem $folder -File -ErrorAction SilentlyContinue
if (-not $files) {
    Write-Host "Tidak ada file di $folder" -ForegroundColor Yellow
    exit 1
}
 
$total = 0
$besar = foreach ($f in $files) {
    $total += $f.Length
    if ($f.Length -gt $batasMb * 1MB) { $f }
}
 
Write-Host "Jumlah file: $($files.Count)"
Write-Host "Total ukuran: $([math]::Round($total / 1MB, 2)) MB"
Write-Host "File di atas $batasMb MB: $($besar.Count)"

This script uses all of this episode's concepts at once: foreach to collect large files, total accumulation, and handling the empty state via if from episode 5.

Conclusion

Episode 6 gives you the repetition engine: foreach for whole collections, ForEach-Object for the pipeline with the $_ variable, -Begin/-Process/-End blocks for aggregation, for with a counter for indices, while for indefinite durations, do-while and do-until guaranteed to run once, plus break, continue, and return to control flow.

Key takeaways:

  • foreach loads all data, ForEach-Object processes streaming — choose based on data size.
  • $_ is the item being processed; don't set it manually.
  • do-while and do-until are guaranteed to execute the block at least once.
  • break stops the loop, continue jumps to the next iteration.
  • Loops + decisions from episode 5 = a script that processes data intelligently.

In episode 7 we package the loop and decision logic into functions & script blocks — named code blocks that can be called, given parameters, and reused without copying code. See you in episode 7!