Functions and Scripts
Reviewed & published by Brayan K
By the end of this lesson you'll wrap reusable logic in named functions with typed, validated, and mandatory parameters, accept input straight from the pipeline, and understand exactly what a function returns — so your tools behave like real cmdlets instead of leaking surprises.
Part of the free PowerShell course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Define a function with the function keyword and Verb-Noun naming
- Declare typed parameters with defaults inside a param() block
- Force required input with [Parameter(Mandatory)]
- Accept pipeline input using ValueFromPipeline + a process {} block
- Know exactly what a function returns (everything it emits) and use return correctly
- Promote a function to an advanced function with [CmdletBinding()]
💡 Real-World Analogy
A function is a kitchen appliance. A blender has labelled inputs (the lid, the speed dial) — those are your param() entries. Some inputs are required: you can't blend without the jug attached, just like [Parameter(Mandatory)]. You feed ingredients in through the top one at a time — that's pipeline input arriving in the process {} block. And the appliance produces one thing: the smoothie that pours out. In PowerShell, everything you pour out is the result — so if you accidentally drop a stray spoon in the jug, it comes out in the smoothie too. That "accidental spoon" is the output-pollution bug you'll learn to avoid.
1. The function keyword & param() blocks
A function is a named, reusable block of code. You write function Verb-Noun { ... } — the same Verb-Noun naming PowerShell uses for built-in cmdlets like Get-Date, which makes your tools feel native. Inputs go in a param() block, where each parameter can declare a type (like [string] or [int]) and a default value. Crucially, you call a function the shell way: space-separated, dash-named arguments like Get-Greeting -Name "Alice" — not with commas in brackets. Read this worked example and run it.
# 'function' gives a block of code a name so you can reuse it.
# Naming convention is Verb-Noun, the same as built-in cmdlets (Get-Date).
function Get-Greeting {
# param() lists the inputs. [string] is the TYPE; = "World" is a DEFAULT.
param([string]$Name = "World")
# Any value you leave "hanging" (not assigned to a variable) is OUTPUT.
"Hello, $Name!"
}
# Call it. PowerShell args are SPACE-separated and named with a leading dash:
Get-Greeting # uses the default -> Hello, World!
Get-Greeting -Name "Alice" # passes a value -> Hello, Alice!
Get-Greeting Bob # positional (no -Name) also works -> Hello, Bob!Your turn. The function below is almost complete — fill in the ___ blanks using the hints, then run it. Notice how a typed parameter with a default lets the caller skip arguments.
# 🎯 YOUR TURN — replace each ___ , then run it.
function Get-Area {
param(
# 1) Give -Width a type of [int] and a default of 1
[___]$Width = ___, # 👉 type is [int], default is 1
# 2) Give -Height a type of [int] and a default of 1
[int]$Height = ___ # 👉 default is 1
)
# 3) Output the area. Wrap maths in $(...) so it calculates inside the text
"Area is $(___)" # 👉 $Width * $Height
}
Get-Area -Width 4 -Height 3
Get-Area # both default to 1
# ✅ Expected output:
# Area is 12
# Area is 12. Required input with [Parameter(Mandatory)] & [CmdletBinding()]
Sometimes a parameter is essential and there's no sensible default. Mark it [Parameter(Mandatory)] and PowerShell will refuse to run without it — if the caller forgets, the shell interactively prompts for the value instead of crashing. Adding [CmdletBinding()] at the top of the param() block promotes your function to an advanced function: it instantly gains the common parameters every real cmdlet has, like -Verbose and -ErrorAction. Write-Verbose messages only appear when the caller asks for them with -Verbose, and they never mix into your data output.
# [Parameter(Mandatory)] forces the caller to supply a value.
# [CmdletBinding()] turns this into an "advanced function" — it now gets
# free common parameters like -Verbose and -ErrorAction, just like a real cmdlet.
function New-Password {
[CmdletBinding()]
param(
[Parameter(Mandatory)] # if you omit -Length, PowerShell PROMPTS for it
[int]$Length,
[string]$Prefix = "pw" # optional, has a default
)
# Write-Verbose only prints when the caller passes -Verbose. It does NOT
# pollute the function's real output — handy for progress messages.
Write-Verbose "Building a password of length $Length"
$chars = -join ((1..$Length) | ForEach-Object { 'x' })
"$Prefix-$chars"
}
New-Password -Length 5
New-Password -Length 3 -Prefix "key"3. Pipeline input: ValueFromPipeline + process {}
PowerShell's superpower is the pipeline (|), and your functions can plug straight into it. Two pieces make it work: mark a parameter [Parameter(ValueFromPipeline)] so values are allowed to arrive from the pipe, and put your logic in a process {} block, which runs once for every item that comes down the pipeline. Inside process, your parameter (and the automatic variable $_) holds the current item. Skip the process block and your function only ever sees the last piped item — a very common beginner bug.
# Make a function accept PIPELINE input with two pieces:
# [Parameter(ValueFromPipeline)] -> "values can arrive from the pipeline"
# a process {} block -> runs ONCE PER item that comes down the pipe
function ConvertTo-Upper {
param(
[Parameter(ValueFromPipeline)]
[string]$Text
)
process {
# Inside process{}, $Text is the CURRENT item. $_ also refers to it.
$Text.ToUpper()
}
}
# Send three strings down the pipeline — process{} fires three times.
"hello", "world", "powershell" | ConvertTo-Upper
# It still works as a normal parameter too:
ConvertTo-Upper -Text "direct"Your turn. Combine what you've learned: make Get-Square accept numbers from the pipeline and require a label. Fill in the three blanks.
# 🎯 YOUR TURN — make Get-Square accept pipeline input and a required label.
function Get-Square {
[CmdletBinding()]
param(
# 1) Mark -Number so values can arrive FROM the pipeline
[Parameter(___)] # 👉 ValueFromPipeline
[int]$Number,
# 2) Make -Label required
[Parameter(___)] # 👉 Mandatory
[string]$Label
)
# 3) This must run once per piped item — name the block correctly
___ { # 👉 process
"$Label $Number = $($Number * $Number)"
}
}
1, 2, 3 | Get-Square -Label "sq"
# ✅ Expected output:
# sq 1 = 1
# sq 2 = 4
# sq 3 = 94. return and what a function really outputs
Here's the idea that trips up everyone coming from C#, Python, or JavaScript: in PowerShell, every value a function emits is part of its return value — not just the thing after return. A bare "Starting..." on its own line is emitted and ends up in the output. return $x doesn't mean "this is THE result"; it means "stop the function now and emit $x". To send a value back you simply leave it un-assigned. To print a human message without polluting the output, use Write-Host or Write-Verbose, which write to separate channels.
# THE BIG RULE: everything a function emits becomes its output.
# 'return' does NOT mean "this is the result" — it just STOPS the function and
# hands back whatever you put after it (which is also added to the output).
function Get-Total {
param([int[]]$Numbers)
"Starting..." # ⚠️ this string is ALSO output (a leak!)
$sum = ($Numbers | Measure-Object -Sum).Sum # assigned -> NOT output
return $sum # stops here and emits $sum
}
# Because "Starting..." was emitted, the caller gets TWO objects back:
$result = Get-Total -Numbers 1, 2, 3
"Captured: $result"
# Fix: don't emit stray values. Use Write-Host/Write-Verbose for messages,
# which are NOT part of the data output.
function Get-CleanTotal {
param([int[]]$Numbers)
Write-Host "Starting..." # message channel, not the output channel
($Numbers | Measure-Object -Sum).Sum
}
$clean = Get-CleanTotal -Numbers 1, 2, 3
"Clean: $clean"Watch the output channel
In the first example, $result captured both "Starting..." and 6 because both were emitted. The fix is to never leave a stray value un-handled: assign it, pipe it to Out-Null, or use Write-Host/Write-Verbose for messages. This is the single most common reason a PowerShell function "returns weird extra stuff".
Deep Dive: variable scope
Variables you create inside a function are local to it — they vanish when the function ends and don't leak out to the caller. That's a good thing: it keeps your functions self-contained.
The twist is that a function can read a variable from the surrounding (parent) scope, but if it assigns to one, it creates a new local copy and leaves the original untouched. So this prints 1, not 99:
$count = 1
function Bump { $count = 99 } # creates a LOCAL $count
Bump
$count # still 1 — the outer one never changedPrefer passing values in as parameters and returning results out — relying on outer variables makes functions fragile. If you truly must change a caller's variable, that's what [ref] parameters or explicit $script: / $global: scopes are for, but reach for them rarely.
Common Errors (and the fix)
- Calling with commas/brackets like other languages: Get-Area(4, 3) does not pass two arguments — it passes one two-item array to the first parameter. PowerShell uses space-separated, dash-named args: Get-Area -Width 4 -Height 3.
- Accidental output pollution: a stray line like "done", or $list.Add(1) (which returns the index), gets added to your output. Symptom: $x = My-Func comes back as an array of unexpected things. Fix: assign it, pipe to | Out-Null, or use Write-Host/Write-Verbose for messages.
- Pipeline input only sees the last item: you used [Parameter(ValueFromPipeline)] but put the logic outside a process {} block, so it ran once on the final item. Fix: move the per-item work into process {}.
- Changing an outer variable from inside a function and it "doesn't stick": assigning inside a function makes a local copy. Return the new value and capture it ($count = Bump $count) instead of mutating the outer one.
- "A parameter cannot be found that matches parameter name 'X'": you passed a -Name the param() block doesn't declare (often a typo). Check the parameter spelling against the function definition.
📋 Quick Reference
| Concept | Syntax |
|---|---|
| Define a function | function Verb-Noun { ... } |
| Typed param + default | param([int]$N = 1) |
| Required param | [Parameter(Mandatory)] |
| Pipeline input | [Parameter(ValueFromPipeline)] + process {} |
| Advanced function | [CmdletBinding()] |
| Return a value | just emit it (or return $x to stop early) |
| Call a function | Verb-Noun -Param value |
| Suppress output | expression | Out-Null |
Mini-Challenge: Convert-Temperature
No blanks this time — just a brief and an outline. Build a pipeline-friendly function from scratch, run it, and check your output against the example in the comments. This is exactly the shape of a real reusable tool.
# 🎯 MINI-CHALLENGE: a Convert-Temperature function
# 1. function Convert-Temperature with [CmdletBinding()]
# 2. Params:
# -Celsius [double] marked [Parameter(Mandatory)] AND ValueFromPipeline
# -Round [int] default 1 (decimal places)
# 3. In a process {} block, work out fahrenheit = $Celsius * 9 / 5 + 32
# and OUTPUT one line: "$Celsius C = <fahrenheit> F"
# (use [math]::Round(<value>, $Round) to round)
# 4. Make sure NOTHING else is emitted (no stray strings -> no output leak).
#
# Try it with: 0, 37, 100 | Convert-Temperature
#
# ✅ Expected output:
# 0 C = 32 F
# 37 C = 98.6 F
# 100 C = 212 F
# your code here🎉 Lesson Complete!
- ✅ function Verb-Noun names reusable code; call it with space-separated, dash-named args
- ✅ param() declares typed parameters with defaults; [Parameter(Mandatory)] makes one required
- ✅ ValueFromPipeline + a process {} block let a function stream the pipeline, once per item
- ✅ Everything a function emits is returned; return only stops it early
- ✅ Use Write-Host/Write-Verbose for messages to avoid output pollution
- ✅ [CmdletBinding()] makes an advanced function with free common parameters
Practice quiz
Which keyword defines a named function in PowerShell?
- function
- def
- sub
- func
Answer: function. You declare a function with the function keyword, e.g. function Get-Greeting { }.
Where do you declare a function's parameters?
- In an args block
- In a param() block
- In the function name
- In a return statement
Answer: In a param() block. Parameters go in a param() block, e.g. param([string]$Name = 'World').
How should you call a function in PowerShell?
- Get-Greeting('Alice')
- Get-Greeting{Alice}
- Get-Greeting -Name 'Alice'
- Get-Greeting=Alice
Answer: Get-Greeting -Name 'Alice'. PowerShell uses space-separated, dash-named arguments, not commas in brackets.
What does [Parameter(Mandatory)] do?
- Sets a default value
- Hides the parameter
- Adds verbose output
- Forces the caller to supply a value
Answer: Forces the caller to supply a value. Mandatory makes the parameter required, prompting if it is omitted.
Which two pieces let a function accept pipeline input?
- ValueFromPipeline plus a process {} block
- Mandatory plus return
- CmdletBinding plus begin
- param plus end
Answer: ValueFromPipeline plus a process {} block. Mark the parameter ValueFromPipeline and put logic in a process {} block.
In PowerShell, what becomes a function's output?
- Only the value after return
- Every value the function emits
- Only Write-Host text
- Nothing unless return is used
Answer: Every value the function emits. Every un-assigned value a function emits is part of its output.
What does the return keyword actually do?
- Marks the only result
- Suppresses all output
- Stops the function and emits what follows it
- Starts a loop
Answer: Stops the function and emits what follows it. return stops the function early and emits its value; it does not mark the sole result.
Which cmdlet prints a message without polluting data output?
- Return-Value
- Write-Verbose
- Emit-Data
- Echo-Line
Answer: Write-Verbose. Write-Verbose (or Write-Host) uses a separate channel from data output.
What does [CmdletBinding()] add to a function?
- A return type
- Faster execution
- Automatic parameters
- Common parameters like -Verbose and -ErrorAction
Answer: Common parameters like -Verbose and -ErrorAction. It promotes the function to an advanced function with common parameters.
Without a process {} block, a pipeline-input function sees which items?
- Every item
- The first item only
- No items
- Only the last piped item
Answer: Only the last piped item. Without process {}, the body runs once and only sees the last piped item.
Continue this course
- Previous: Working with Objects
- Next: File System and Registry — Navigate drives, manage files and folders, and read/write the Windows registry
Frequently asked questions
Why does Get-Greeting -Name 'Alice' use a space and a dash, not ("Alice")?
PowerShell is a shell, so it parses commands like a command line, not like C# or Python. Arguments are separated by spaces and named with a leading dash (-Name). Writing Get-Greeting("Alice") passes a single array argument to the first positional parameter, and Get-Greeting("Alice", "Bob") passes ONE two-item array — almost never what you want. Always call functions as: Name -Param value -Param2 value2.
Do I need 'return' at the end of a function?
No. In PowerShell every value you leave un-assigned is automatically added to the output, so the last expression is already returned. 'return' only stops the function early — it is not how you mark 'the result'. Use it for guard clauses (return early if input is bad), not at the natural end of a function.
My function returns extra junk I didn't ask for. Why?
Output pollution. Any expression that isn't captured into a variable, piped, or sent to Out-Null becomes part of the output — including a stray string, or the return value of something like $list.Add(1). Assign such values away, pipe them to Out-Null, or use Write-Host / Write-Verbose for human messages so they don't mix into the data you return.
What is the difference between begin, process, and end blocks?
When a function takes pipeline input you can split its body into three blocks. begin {} runs once before any items, process {} runs once for EACH item coming down the pipeline (this is where $_ / your ValueFromPipeline parameter holds the current item), and end {} runs once after all items. If you don't write these blocks, your whole function body behaves like an end block and only sees the LAST piped item — which is a classic pipeline bug.
What does [CmdletBinding()] actually give me?
It promotes a basic function into an 'advanced function' that behaves like a compiled cmdlet. You get the common parameters for free — -Verbose, -Debug, -ErrorAction, -ErrorVariable, -WhatIf/-Confirm (with SupportsShouldProcess) — plus stricter parameter binding. It costs you nothing, so it's good practice on any function that takes parameters.