Control Flow: Conditionals, Loops & Switch

PowerShell Fundamentals

Chapter 6 · Control Flow: Conditionals, Loops & Switch

Chapter 4 already introduced ForEach-Object — but strictly as one stage inside a pipeline, using $_ to touch each object flowing through it. This chapter is about writing an actual script body: if/elseif/else, a genuinely more capable switch statement than most languages ship with, and a foreach loop keyword that looks almost identical to Chapter 4's cmdlet by name — while behaving differently enough underneath that mixing the two up is one of the most common early PowerShell mistakes.

if / elseif / else

Braces, not indentation (unlike Python) and not then/fi keywords (unlike Bash) — conditions go in parentheses, bodies go in braces:

if ($age -ge 65) { "Senior" } elseif ($age -ge 18) { "Adult" } else { "Minor" }

switch: PowerShell's Own Surprisingly Powerful Version

A basic switch looks familiar from other languages, but it comes with real extras — wildcard matching, regex matching, and (unlike almost anything else called "switch") the ability to iterate an entire array directly:

# Basic value matching switch ($status) { "Running" { "Service is up" } "Stopped" { "Service is down" } default { "Unknown status" } } # -Wildcard — the same * / ? matching Chapter 4's -like used switch -Wildcard ($filename) { "*.txt" { "Text file" } "*.log" { "Log file" } default { "Other" } } # Handed an array instead of a single value, switch iterates it automatically switch (@(1, 2, 3, 4)) { { $_ % 2 -eq 0 } { "$_ is even" } default { "$_ is odd" } } # 1 is odd / 2 is even / 3 is odd / 4 is even — all four run, one per array element
switch runs every matching clause, not just the first one
Most languages' switch stops at the first match unless you fall through on purpose. PowerShell's does the opposite by default: every clause whose condition matches actually runs, one after another, for the same input. switch ($x) { {$_ -gt 0} {"positive"} {$_ -gt 10} {"big"} } run with $x = 15 prints both "positive" and "big" — both conditions are true for 15. If you want only the first match to run, add break at the end of each clause body.

foreach: The Loop Keyword (Not the Cmdlet)

foreach written as a bare keyword — no pipe, no -Object — is a completely different piece of syntax from Chapter 4's ForEach-Object, despite the near-identical name:

foreach ($p in Get-Process) { "$($p.ProcessName) is using $($p.CPU) seconds of CPU" }
The central distinction this chapter is built on
foreach is a language statement: it fully evaluates Get-Process into a complete, in-memory collection first, then loops over that finished collection. Get-Process | ForEach-Object {...} (Chapter 4) is a pipeline stage: it processes each process object as soon as it arrives, one at a time, without ever holding the full set in memory at once. On a small result set the difference is invisible. On something genuinely large — every row from a multi-million-row query, or every file under a massive directory tree — ForEach-Object's streaming behavior can matter a great deal, while a bare foreach has to build the whole collection before the loop body runs even once.

while, do-while & do-until

# while — checks the condition BEFORE each pass; may run zero times $i = 0 while ($i -lt 3) { "i is $i" $i++ } # do-while — checks AFTER each pass, so the body always runs at least once do { "This runs even if the condition starts false" } while ($false) # do-until — same "runs at least once" shape, but inverted logic: # do-while continues WHILE true; do-until continues UNTIL true (i.e. while false) do { "Also runs at least once" } until ($true)

do-while and do-until are easy to swap by mistake specifically because their loop shape is identical — only the condition's meaning flips (continue while true vs. continue until true, i.e. while false). Reading the condition out loud in plain English before choosing which one to use avoids the mix-up.

for: The Classic Three-Part Loop

for ($i = 0; $i -lt 5; $i++) { "i is $i" }

break & continue

Both work exactly as they do in most C-family languages — break exits the loop (or, per the warn-box above, a switch clause) entirely; continue skips straight to the next iteration:

foreach ($n in 1..10) { if ($n -eq 7) { break } # stop the whole loop at 7 if ($n % 2 -eq 0) { continue } # skip even numbers "$n" } # 1, 3, 5

Iterating a Hashtable, Properly

Chapter 5 introduced @{}/[ordered]@{} but deferred looping over one — here's the real pattern, using .GetEnumerator() rather than trying to foreach the hashtable directly:

$person = [ordered]@{ Name = "Alice"; Age = 30 } foreach ($entry in $person.GetEnumerator()) { "$($entry.Key) = $($entry.Value)" } # Name = Alice # Age = 30
A first practical habit
When choosing between foreach and ForEach-Object, ask one question: is this collection already sitting fully in memory (an array, a hashtable) or is it streaming out of a pipeline (Get-ChildItem on a huge tree, a database query)? Already-in-memory favors foreach; a live pipeline favors ForEach-Object.

Hands-On Exercises

Exercise 1

Using this chapter's own array-iteration example, explain why switch (@(1, 2, 3, 4)) { {$_ % 2 -eq 0} {"$_ is even"} default {"$_ is odd"} } produces four separate results rather than just one.

📄 View solution
Exercise 2

Given switch ($x) { {$_ -gt 0} {"positive"} {$_ -gt 10} {"big"} } run with $x = 15, explain why both "positive" and "big" print. Then explain how you'd change the code so only "big" prints.

📄 View solution
Exercise 3

Explain the practical difference between foreach ($p in Get-Process) {...} and Get-Process | ForEach-Object {...}. Describe a concrete situation where that difference would actually matter, not just a situation where it happens to be invisible.

📄 View solution

Chapter 6 Quick Reference

  • if / elseif / else — parentheses around the condition, braces around the body
  • switch — supports -Wildcard/-Regex, auto-iterates an array; runs every matching clause unless you add break
  • foreach ($x in $collection) — a loop keyword; evaluates the whole collection into memory first
  • ForEach-Object (Chapter 4) — a pipeline cmdlet; streams one object at a time, never holding the whole set in memory
  • while — checks before each pass, may run zero times
  • do-while / do-until — always run at least once; inverted continue-condition logic (while true vs. until true)
  • for ($i=0; $i -lt N; $i++) — the classic three-part loop
  • break / continue — exit entirely / skip to next iteration; break also stops a switch clause from letting later clauses run
  • $hash.GetEnumerator() — the correct way to foreach over a hashtable's key/value pairs
  • Next chapter: Functions & Script Files (.ps1)