Adding [CmdletBinding()] to a function turns it into an advanced function. This gives you -Verbose, -Debug, -WhatIf, -Confirm, and -ErrorAction for free—you do not have to implement them yourself. Write-Verbose inside the function produces output only when the caller passes -Verbose. This makes production debugging dramatically easier without changing the function's code.
Functions, Parameters, and Modules
Write reusable, parameterized functions with typed inputs and validation, package them into .psm1 modules, and build a shared automation library that multiple scripts and teams can consume.
🧒 Simple Explanation (ELI5)
A function is a saved recipe with its own name. Instead of writing the same ten steps every time you want a certain dish, you call the recipe by name and it handles everything. Parameters are the ingredients you pass in: "make this recipe for 4 people, not 2." A module is a cookbook: a collection of recipes packaged together so your whole team can use them.
🔧 Why Do We Need It?
- Avoid repetition: deploy-to-azure logic written once in a function is called from dozens of scripts instead of copied and drifting.
- Parameter validation: enforcing types and ValidateSet catches bad inputs before they reach Azure, file systems, or live services.
- Discoverability: advanced functions with comment-based help are documented with Get-Help, just like built-in cmdlets.
- Team reusability: a shared .psm1 module installed in the team's PowerShell module path means every engineer uses the same vetted functions.
⚙️ Technical Explanation
A basic function is declared with the function keyword. An advanced function adds a [CmdletBinding()] attribute and a param() block, which grants access to -Verbose, -Debug, -WhatIf, -ErrorAction, and other common parameters automatically.
The param() block declares typed, validated, and optionally mandatory parameters. Common validation attributes: [Parameter(Mandatory)], [ValidateSet('dev','staging','prod')], [ValidateRange(1,65535)], [ValidateNotNullOrEmpty()].
A module is a .psm1 file containing one or more functions. Use Export-ModuleMember to control which functions are public. Install to the user module path so Import-Module can find it. Modules can include a .psd1 manifest with version, author, and dependency information.
Keep one function per file in a module folder, then dot-source them all in the .psm1 with Get-ChildItem $PSScriptRoot\functions -Filter *.ps1 | ForEach-Object { . $_.FullName }. This keeps individual functions easy to test, version, and review in pull requests without opening one large file.
📊 Visual Representation
⌨️ Commands / Syntax
# Advanced function with full parameter validation
function Deploy-WebApp {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateSet('dev','staging','prod')]
[string]$Environment,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$AppName,
[Parameter()]
[ValidateRange(1, 100)]
[int]$Instances = 2
)
Write-Verbose "Deploying $AppName to $Environment with $Instances instances"
if ($PSCmdlet.ShouldProcess("$AppName in $Environment", "Deploy")) {
# Deployment logic here
Write-Host "Deployed $AppName to $Environment"
}
}
# Call the function
Deploy-WebApp -Environment prod -AppName "skilly-api" -Instances 4 -Verbose
Deploy-WebApp -Environment prod -AppName "skilly-api" -WhatIf
# Function that returns objects
function Get-DiskReport {
[CmdletBinding()]
param([string[]]$ComputerName = @("localhost"))
foreach ($computer in $ComputerName) {
Get-PSDrive -PSProvider FileSystem -CimSession $computer |
Select-Object @{n="Computer";e={$computer}}, Name,
@{n="UsedGB";e={[math]::Round($_.Used/1GB,1)}},
@{n="FreeGB";e={[math]::Round($_.Free/1GB,1)}},
@{n="UsedPct";e={[math]::Round($_.Used/($_.Used+$_.Free)*100,1)}}
}
}
# Create and import a module
# 1. Save functions to C:\Modules\TeamTools\TeamTools.psm1
# 2. Import:
Import-Module C:\Modules\TeamTools\TeamTools.psm1
# 3. Or copy to module path and import by name:
Import-Module TeamTools
# List exported functions in a module
Get-Command -Module TeamTools
# Get help (works when you add comment-based help to your function)
Get-Help Deploy-WebApp -Examples
💼 Example (Real-world Use Case)
A platform team has 12 engineers all writing their own Azure deployment snippets. They standardize on a shared AzureOps module that exports Deploy-AzWebApp, Get-AzHealthStatus, and Remove-AzStagingSlot. Every engineer imports the module from a shared network path. Bug fixes go in one place. New parameters get added to the module, not to 12 individual scripts.
🧪 Hands-on
- Write a function
Get-DiskAlertthat takes a[string]$ComputerNameand a[int]$ThresholdPct = 80parameter and returns drives above the threshold. - Add
[CmdletBinding()]and test with-Verboseto see Write-Verbose output. - Add
[ValidateRange(50,99)]to the threshold parameter and try calling it with a value of 101 to see the validation error. - Save the function to a .psm1 file and import it with Import-Module. Confirm it appears in Get-Command.
- Add comment-based help and verify Get-Help shows your synopsis.
Write an advanced function Test-ServiceHealth that accepts a mandatory [string[]]$ServiceName parameter using [Parameter(Mandatory)] and [ValidateNotNullOrEmpty()]. The function should check each service, output a [PSCustomObject] with ServiceName, Status, and IsHealthy fields, and pipe the results so callers can filter and export them. Call it with Test-ServiceHealth -ServiceName wuauserv,bits | Where-Object IsHealthy -eq $false.
🐛 Debugging Scenario
Problem: a function parameter marked [Parameter(Mandatory)] prompts interactively for a value even when the script runs in a CI/CD pipeline, causing the pipeline to hang.
- Cause: the function was called without providing the mandatory parameter. PowerShell falls back to interactive prompting when it cannot get the value automatically.
- Diagnose: check the pipeline invocation command to see if the parameter was passed. Look for typos in the parameter name (case-insensitive but the name must match).
- Fix: add a validation step in the pipeline to confirm required parameters are present before calling the function. In CI/CD pipelines, pass all mandatory parameters explicitly and use
-ErrorAction Stopso missing parameters throw immediately rather than prompting.
🎯 Interview Questions
Beginner
function FunctionName { ... }. Parameters go in a param() block inside the function body. Use the function keyword followed by the name and a script block.
An advanced function adds [CmdletBinding()] before the param() block. This gives it automatic support for -Verbose, -Debug, -WhatIf, -Confirm, -ErrorAction, and -ErrorVariable, and makes it behave like a built-in cmdlet.
Add [Parameter(Mandatory)] above the parameter declaration in the param() block. PowerShell will prompt for it if it is not supplied. In scripts, always pass mandatory parameters explicitly—never rely on interactive prompting in automation.
Intermediate
ValidateSet restricts a parameter to a list of allowed values. [ValidateSet('dev','staging','prod')] means passing any other string throws a validation error immediately, before any deployment code runs. This prevents typos like "prodution" from silently targeting the wrong environment.
SupportsShouldProcess adds -WhatIf and -Confirm support to your function. Wrap destructive operations with if ($PSCmdlet.ShouldProcess(target, action)). When -WhatIf is passed, the block is skipped and the action is only printed. Use it on any function that modifies state: deleting files, stopping services, removing Azure resources.
Create a folder with the module name, a .psm1 file with the functions (or dot-sources individual .ps1 files), optionally a .psd1 manifest, and optionally a tests/ subfolder. Export public functions with Export-ModuleMember. Install to $env:PSModulePath so Import-Module finds it by name.
Scenario-based
Extract the common patterns into a shared module stored in a team repository. Add it to a shared network path or publish it to a private PowerShell feed. Engineers import it by name. Version it with a .psd1 manifest. Add comment-based help so everyone uses Get-Help. Code review changes through pull requests like any other code.
Normalize the input at the start of the function: $Environment = $Environment.ToLower(). Better yet, use [ValidateSet] which is case-insensitive by default—"PROD" passes validation against 'prod'. In switch statements, PowerShell comparisons are also case-insensitive by default unless you use -CaseSensitive.
🌐 Real-world Usage
Enterprise teams build internal modules containing functions for Azure resource provisioning, Active Directory user management, certificate rotation, and deployment orchestration. These modules are version-controlled, published to internal artifact feeds (Azure Artifacts or ProGet), and consumed by pipeline tasks the same way public modules are consumed from PSGallery.
📝 Summary
Functions encapsulate reusable logic. Advanced functions add [CmdletBinding()] for automatic common parameter support. The param() block declares typed, validated parameters. ValidateSet, ValidateRange, and[Parameter(Mandatory)] prevent bad inputs before they cause damage. Modules package functions for team reuse. SupportsShouldProcess enables -WhatIf on any function that modifies state.