PowerShell · lesson 4 of 12
Control Flow, Functions, and Pipeline Input
PowerShell's switch tests every matching case by default, not just the first — and a function that accepts pipeline input silently drops every item but the last until it gets a process block.
By SkillByExample · Published
Two of PowerShell’s control-flow features behave differently from their nearest bash or C equivalent — quietly enough that both are easy to get wrong the first time.
switch tests every case, not just the first match
$number = 10
switch ($number) {
{ $_ % 2 -eq 0 } { "even" }
{ $_ -gt 5 } { "greater than 5" }
default { "other" }
}
even
greater than 5
Both blocks ran. Unlike a C-style switch or bash’s case, PowerShell’s switch doesn’t stop
after the first match by default — it evaluates every case against the value and runs every
one that matches. Add break inside a case body to get the traditional stop-at-first-match
behavior back.
switch also matches more than exact equality out of the box:
$file = "report.csv"
switch -Wildcard ($file) {
"*.csv" { "spreadsheet data" }
"*.txt" { "plain text" }
default { "unknown type" }
}
spreadsheet data
-Wildcard (or -Regex) turns each case label into a pattern instead of a literal value —
this one switch replaces what would otherwise be a chain of if/elseif with string
matching.
foreach vs ForEach-Object
foreach ($i in 1..3) { $i * $i }
1
4
9
foreach is a language keyword — it expects a collection already available in memory and
runs in the current scope, with break and continue working exactly like they do in any
other language’s loop. ForEach-Object is a cmdlet, meant to sit in the middle of a pipeline:
1..3 | ForEach-Object { $_ * $_ }
1
4
9
Same result, different mechanism — ForEach-Object receives each value as the previous stage
of the pipeline produces it, one at a time, rather than requiring the whole sequence up front.
That matters once the upstream cmdlet is something like Get-ChildItem -Recurse on a large
tree: results can start flowing before the scan finishes.
Functions and parameters
function Get-Greeting {
param(
[string]$Name = "World",
[switch]$Shout
)
$greeting = "Hello, $Name!"
if ($Shout) { $greeting.ToUpper() } else { $greeting }
}
Get-Greeting
Get-Greeting -Name "Dune" -Shout
Hello, World!
HELLO, DUNE!
[switch]$Shout is a flag, not a boolean you pass a value to — -Shout on its own turns it
on; there’s no -Shout $true needed.
The bug: pipeline input without a process block
function Show-Name {
param(
[Parameter(ValueFromPipeline=$true)]
[string]$Name
)
"Hello, $Name"
}
"Alice","Bob","Carol" | Show-Name
Hello, Carol
Three names went in; one came out. Without a process block, a function’s entire body is an
implicit end block — it runs exactly once, after the pipeline has finished delivering
every value, by which point $Name holds only the last one PowerShell bound to it.
The fix: add a process block
function Show-Name {
param(
[Parameter(ValueFromPipeline=$true)]
[string]$Name
)
process {
"Hello, $Name"
}
}
"Alice","Bob","Carol" | Show-Name
Hello, Alice
Hello, Bob
Hello, Carol
process tells PowerShell to run that block once per incoming object, which is what makes a
function behave like a real pipeline stage instead of a plain subroutine that only sees
whatever arrived last.
Practice
1. Reject an invalid parameter value with ValidateSet and read the real error.
function Set-LogLevel {
param(
[ValidateSet("Info","Warning","Error")]
[string]$Level
)
"Log level set to $Level"
}
Set-LogLevel -Level "Warning"
Set-LogLevel -Level "Critical"
Log level set to Warning
Cannot validate argument on parameter 'Level'. The argument "Critical" does not belong to the set "Info,Warning,Error" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
ValidateSet rejects the bad value before the function body ever runs — the error names the
parameter, the rejected value, and the full allowed set, all without a single if written by
hand.
2. Write a function using begin, process, and end together to sum piped values.
function Measure-Total {
param(
[Parameter(ValueFromPipeline=$true)]
[int]$Value
)
begin { $total = 0 }
process { $total += $Value }
end { "Total: $total" }
}
10, 20, 30 | Measure-Total
Total: 60
begin initializes the accumulator once, process runs once per number adding to it, and
end reports the final result once everything has passed through — three distinct moments in
the pipeline’s lifetime, each with exactly one job.
3. Use switch -Regex to categorize a string by pattern instead of exact value.
$line = "ERROR: connection refused"
switch -Regex ($line) {
"^ERROR" { "this is an error line" }
"^WARN" { "this is a warning line" }
default { "unclassified" }
}
this is an error line
Same switch, same default-tests-every-case behavior — -Regex just changes what counts as
a match, the same way -Wildcard did earlier in this lesson.
Next: working with files, CSV, and JSON — where the object pipeline stops needing any parsing at all.