Summary: in this tutorial, you will learn learn to write powershell functions with parameters, validation, cmdletbinding, and advanced function features.
Functions and scripts are where PowerShell transitions from interactive command-line tool to full programming language. They're how you package logic into reusable components, build tools your team can share, and create maintainable automation that grows with your needs.
This tutorial takes you from simple one-liner functions to production-quality advanced functions with parameter validation, pipeline support, and proper error handling — the same capabilities built into PowerShell's own cmdlets.
Before diving into syntax, understand what functions solve:
Without functions, you repeat the same code patterns throughout scripts. When requirements change, you must update each copy. When bugs appear, you fix them multiple times. Code becomes unmaintainable.
With functions, you write logic once, call it everywhere. Changes happen in one place. Testing is isolated. Code becomes self-documenting when functions have clear names describing their purpose.
function Say-Hello { "Hello, World!"}Say-Hello # Output: Hello, World!
This function takes no inputs and produces a single output. The last (only) statement in the function — the string "Hello, World!" — becomes the function's return value.
function Say-HelloTo { param( [string]$Name ) "Hello, $Name!"}Say-HelloTo -Name "Alice" # Hello, Alice!Say-HelloTo "Alice" # Same result (positional)
The param() block declares parameters. Each parameter has a type ([string]) and a name ($Name). PowerShell automatically creates variables for each parameter.
You can call functions two ways:
Named: -Name "Alice" — explicit and clear
Positional: "Alice" — concise but relies on parameter order
For scripts others will use, named parameters improve readability. For personal quick functions, positional is convenient.
function Get-Greeting { param( [string]$Name, [string]$TimeOfDay = "day" # Default value ) "Good $TimeOfDay, $Name!"}Get-Greeting -Name "Bob" # Good day, Bob!Get-Greeting -Name "Bob" -TimeOfDay "morning" # Good morning, Bob!Get-Greeting "Charlie" "evening" # Good evening, Charlie! (positional)
Default values make parameters optional. If the caller doesn't provide -TimeOfDay, it defaults to "day". This lets you design functions with sensible defaults while allowing customization when needed.
Why defaults matter: They simplify common cases. Get-Greeting "Bob" uses the default "day", keeping calls concise. Power users can override: Get-Greeting "Bob" "evening".
PowerShell's approach to return values differs from many languages and is crucial to understand:
function Get-Square { param([int]$Number) $Number * $Number # This expression's result is output}$result = Get-Square -Number 5$result # 25
There's no explicit return keyword needed. The expression $Number * $Number evaluates to a value, and since that value isn't assigned to anything, it flows to the output stream. When you call the function and assign it ($result = Get-Square -Number 5), you capture that output.
Critical concept: Everything in a function that produces output goes to the return value:
function Get-Info { "Starting..." # ← This is output! $data = Get-Date "Processing..." # ← This too! $data # ← And this!}$result = Get-Info$result.Count # 3 — array of three items!
The function returns an array containing three elements: "Starting...", "Processing...", and the DateTime object. This behavior surprises developers from other languages.
PowerShell's #1 gotcha: Unintentional output
Any expression not assigned to a variable becomes output. This means:
Strings written with "text" or 'text' → output
Variables referenced without assignment ($myVar) → output
Command results not assigned (Get-Date) → output
Solution: Use Write-Host for messages that shouldn't be in return value, or use Write-Verbose/Write-Debug for diagnostic messages (covered later).
function Get-InfoCorrect { Write-Host "Starting..." -ForegroundColor Cyan # Display only, not output $data = Get-Date Write-Verbose "Processing..." # Verbose stream, not output $data # Only this becomes the return value}$result = Get-InfoCorrect # $result contains only the DateTime object
The second behavior is the main reason to use return — early exit:
function Find-FirstMatch { param([string[]]$List, [string]$Pattern) foreach ($item in $List) { if ($item -like $Pattern) { return $item # Exit immediately when found } } return $null # No match found}
Without return, you'd need to track state or use labels to exit the loop and function.
However, this function is equivalent (last expression is output):
function Test-Even2 { param([int]$Number) $Number % 2 -eq 0 # Evaluates to $true or $false}
PowerShell convention: Use return for early exits, omit it when the last line naturally produces the result.
PowerShell provides a rich set of validation attributes:
function New-Employee { param( # Must not be null or empty string [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, # Must be one of these specific values [ValidateSet("Developer", "Designer", "Manager", "Admin")] [string]$Role = "Developer", # Must be within numeric range [ValidateRange(18, 120)] [int]$Age, # Must match regex pattern (email validation) [ValidatePattern('^[\w.+-]+@[\w.-]+\.\w+$')] [string]$Email, # Must satisfy custom condition (path must exist) [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$ResumeFilePath, # String length constraints [ValidateLength(3, 20)] [string]$Username, # Array element count constraints [ValidateCount(1, 5)] [string[]]$Skills, # Must not be null (but can be empty) [ValidateNotNull()] [object]$Metadata = @{} ) [PSCustomObject]@{ Name = $Name Username = $Username Role = $Role Age = $Age Email = $Email Skills = $Skills }}
Why validation attributes are powerful:
Tab completion: With [ValidateSet], pressing Tab cycles through valid values:
New-Employee -Name "Alice" -Age 15# Error: Cannot validate argument on parameter 'Age'. The 15 argument is less than the minimum allowed range of 18.
No manual validation code: Instead of:
if ($Age -lt 18 -or $Age -gt 120) { throw "Age must be between 18 and 120"}
You write: [ValidateRange(18,120)] — one line, more maintainable.
Validation before function logic: Parameters are validated before your function code runs. If validation fails, your function never executes, preventing invalid state.
Adding [CmdletBinding()] transforms a simple function into an advanced function (also called a cmdlet function). Advanced functions gain the same capabilities as compiled cmdlets written in C#.
Advanced functions can process pipeline input efficiently using three special blocks:
function Test-PipelineDemo { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string]$Item ) begin { Write-Host "BEGIN: Runs once before processing" -ForegroundColor Cyan $count = 0 } process { Write-Host "PROCESS: Processing '$Item'" -ForegroundColor Yellow $count++ } end { Write-Host "END: Runs once after all processing" -ForegroundColor Green Write-Host "Processed $count items total" }}# Test it"Apple", "Banana", "Cherry" | Test-PipelineDemo
Output:
BEGIN: Runs once before processing
PROCESS: Processing 'Apple'
PROCESS: Processing 'Banana'
PROCESS: Processing 'Cherry'
END: Runs once after all processing
Processed 3 items total
How it works:
begin {}: Runs once before any pipeline items are processed. Use for initialization (opening files, setting up connections, initializing counters).
process {}: Runs once for each pipeline item. This is where you handle each input.
end {}: Runs once after all pipeline items are processed. Use for cleanup (closing files, finalizing output, displaying summaries).
Why this matters: Efficient memory usage. The process block handles one item at a time, so your function can process millions of items without loading them all into memory.
ValueFromPipeline: Accepts the entire piped object:
function Write-Upper { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string]$Text ) process { $Text.ToUpper() }}"hello", "world" | Write-Upper# HELLO# WORLD
ValueFromPipelineByPropertyName: Binds parameter to a property of the piped object:
function Get-FileDetails { [CmdletBinding()] param( [Parameter(ValueFromPipelineByPropertyName)] [Alias("FullName")] [string]$Path ) process { if (Test-Path $Path) { Get-Item $Path | Select-Object Name, Length, LastWriteTime } }}# Get-ChildItem returns objects with a FullName propertyGet-ChildItem C:\Windows\*.log | Get-FileDetails
Get-ChildItem outputs file objects with a FullName property. Because the $Path parameter has ValueFromPipelineByPropertyName and an alias FullName, PowerShell automatically binds each file's FullName property to the $Path parameter.
Rule of thumb:
Use ValueFromPipeline for simple types (strings, numbers) or when you want the whole object
Use ValueFromPipelineByPropertyName when working with objects that have properties matching your parameter names
# See what would be deleted without actually deletingRemove-OldLogFiles -Path "C:\Logs" -WhatIf# What if: Performing the operation "Delete file" on target "C:\Logs\app-2024-01-15.log".# What if: Performing the operation "Delete file" on target "C:\Logs\app-2024-01-16.log".# Prompt for confirmation before each deletionRemove-OldLogFiles -Path "C:\Logs" -Confirm# Actually delete (no prompts)Remove-OldLogFiles -Path "C:\Logs"
$PSCmdlet.ShouldProcess($target, $action) returns $true if the action should proceed. It handles -WhatIf and -Confirm logic automatically.
Best practice: Always add SupportsShouldProcess to functions that:
The ShellRAG editorial team writes practical, beginner-friendly PowerShell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.