Skip to main content
PowerShell · lesson 4 of 12
PowerShell beginner 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

powershell functions switch pipeline control-flow

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.

Frequently Asked Questions

Does PowerShell's switch statement fall through like C's, or stop like bash's case?
Neither exactly — by default it tests every case against the value and runs the block for every one that matches, with no explicit fallthrough needed. Put break inside a case body to stop after the first match, the way a traditional switch works.
What's the actual difference between foreach and ForEach-Object?
foreach is a language keyword that loops over a collection already sitting in memory, in the current scope — fast, and break/continue work exactly as expected. ForEach-Object is a cmdlet meant for the middle of a pipeline: it receives one object at a time as the previous cmdlet produces them, which matters when the upstream command streams results rather than returning them all at once.
Why did piping three names into a function only print the last one?
Without a process block, everything in a function's body is a single implicit end block — it runs exactly once, after the whole pipeline has finished delivering values, by which point the ValueFromPipeline parameter only still holds the final item. A process block tells PowerShell to run that code once per incoming object instead of once total.
What do begin and end blocks add beyond process?
begin runs once before the first pipeline item arrives — the right place to initialize an accumulator or open a connection — and end runs once after the last item has been processed, for a final result or cleanup. Between them, process runs once per item. Omitting begin and end is fine when a function has no setup or wrap-up to do.
Is [switch]$Shout the same thing as a boolean parameter?
Almost — a [switch] parameter is present or absent on the command line (-Shout, no value needed) rather than requiring -Shout $true, which is the more common calling convention for flags in PowerShell. Internally it behaves like a boolean in an if, but callers never type $true or $false for it.