IntermediateLesson 5 of 16

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?

⚙️ 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.

Always Use [CmdletBinding()] for Production Functions

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.

📦
Module Best Practice

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

Module Structure
TeamTools.psm1
Deploy-AzAppService
Get-ServiceHealth
Send-AlertEmail
→ Import-Module
Any Script
→ calls functions
Results

⌨️ Commands / Syntax

powershell
# 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

  1. Write a function Get-DiskAlert that takes a [string]$ComputerName and a [int]$ThresholdPct = 80 parameter and returns drives above the threshold.
  2. Add [CmdletBinding()] and test with -Verbose to see Write-Verbose output.
  3. Add [ValidateRange(50,99)] to the threshold parameter and try calling it with a value of 101 to see the validation error.
  4. Save the function to a .psm1 file and import it with Import-Module. Confirm it appears in Get-Command.
  5. Add comment-based help and verify Get-Help shows your synopsis.
🎮
Try It Yourself

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.

🎯 Interview Questions

Beginner

How do you declare a function in PowerShell?

function FunctionName { ... }. Parameters go in a param() block inside the function body. Use the function keyword followed by the name and a script block.

What is the difference between a basic and an advanced function?

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.

How do you make a parameter mandatory?

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

What is ValidateSet and how does it prevent bad deployments?

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.

What is SupportsShouldProcess and when should you use it?

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.

How do you structure a PowerShell module?

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

You have 12 engineers all writing their own Azure deployment helper functions. What is the best way to standardize?

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.

A function deployed to production receives an environment parameter value of "PROD" (uppercase) and fails because your switch uses lowercase. How do you prevent this?

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.