Skip to content
6 min read·Lesson 3 of 8

Functions, Modules, and Scripts

Going from one-liners to reusable code: functions with parameters, advanced functions, modules, and the script execution model.

A script is fine for one task. The second time you need the same logic you should reach for a function. The third time, a module. PowerShell has a graceful progression from "interactive line" to "published module on the gallery."

Basic Function

function Get-Greeting {
    param(
        [string]$Name = "world"
    )
    "Hello, $Name!"
}

Get-Greeting
Get-Greeting -Name "Alice"
Get-Greeting Alice              # positional

The function emits whatever its last expression returns. return exists but is rarely needed — anything that "falls out" goes to the pipeline.

Advanced Functions

Add [CmdletBinding()] at the top and the function becomes an "advanced function" — it gains all the common parameters (-Verbose, -ErrorAction, -WhatIf, -Confirm) and the parameter machinery cmdlets enjoy.

function Get-LogSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,

        [Parameter()]
        [ValidateSet("ERROR", "WARN", "INFO")]
        [string]$Level = "ERROR",

        [Parameter()]
        [int]$Top = 10
    )

    Write-Verbose "Reading $Path for level $Level"
    Get-Content $Path |
        Select-String -Pattern $Level |
        Group-Object |
        Sort-Object Count -Descending |
        Select-Object -First $Top
}

Get-LogSummary -Path app.log -Level WARN -Verbose

Key attributes:

  • [Parameter(Mandatory)] — required parameter; PowerShell prompts if omitted
  • [Parameter(Position = 0)] — positional
  • [ValidateSet(...)] — restrict to a list of values; tab-completes
  • [ValidateRange(1, 100)] — numeric range
  • [ValidatePattern("^\d+$")] — regex
  • [ValidateScript({ ... })] — arbitrary validation; $_ is the value
  • [ValidateNotNullOrEmpty()] — common-case validation

Validation happens before the function body runs and produces clean error messages.

Pipeline Input

Make a function accept pipeline input by marking a parameter:

function Test-Reachable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$ComputerName
    )
    process {
        if (Test-Connection $ComputerName -Count 1 -Quiet) {
            [pscustomobject]@{ ComputerName = $ComputerName; Reachable = $true }
        } else {
            [pscustomobject]@{ ComputerName = $ComputerName; Reachable = $false }
        }
    }
}

"github.com", "example.com", "doesnotexist.invalid" | Test-Reachable

Three special blocks make pipeline-binding work properly:

  • begin { } — runs once before processing
  • process { } — runs once per pipeline item
  • end { } — runs once after

For pipeline-aware functions, the body goes in process. Plain functions without these blocks process all items at once — usually not what you want for streaming.

Comment-Based Help

Add a structured comment block above the function and Get-Help shows it:

function Get-Greeting {
<#
.SYNOPSIS
Returns a greeting message.

.DESCRIPTION
Builds a greeting using the Name parameter. Defaults to "world".

.PARAMETER Name
The name to greet.

.EXAMPLE
Get-Greeting -Name "Alice"

.OUTPUTS
System.String
#>
    param([string]$Name = "world")
    "Hello, $Name!"
}

Get-Help Get-Greeting -Examples

Production functions deserve full help. Microsoft's authoring guidance is consistent and expected by reviewers.

Scripts

A script is a .ps1 file. Run it directly: ./deploy.ps1 -Environment prod. Scripts can declare a param(...) block at the top:

# deploy.ps1
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateSet("dev", "staging", "prod")]
    [string]$Environment,

    [switch]$DryRun
)

Write-Verbose "Deploying to $Environment"
if ($DryRun) { "Would deploy to $Environment"; return }

# real work...

[switch] is the type for boolean flags — -DryRun on the command line.

Modules

A module bundles related functions. Two kinds:

  • Script module — a .psm1 file containing functions, sometimes with a .psd1 manifest
  • Binary module — a compiled .NET DLL exposing cmdlets (rare for ordinary users)

Minimal script module:

# MyTools/MyTools.psm1
function Get-PublicIP {
    (Invoke-RestMethod ifconfig.me/ip).Trim()
}

function Get-DiskFree {
    [CmdletBinding()]
    param([string]$Path = "/")
    [pscustomobject]@{
        Path = $Path
        FreeGB = [math]::Round((Get-PSDrive (Split-Path $Path -Qualifier).TrimEnd(":")).Free / 1GB, 2)
    }
}

Export-ModuleMember -Function Get-PublicIP, Get-DiskFree

Place under $HOME/Documents/PowerShell/Modules/MyTools/ (Windows) or ~/.local/share/powershell/Modules/MyTools/ (macOS/Linux). PowerShell auto-discovers and lazy-loads it: typing Get-PublicIP imports the module if needed.

Module Manifest (.psd1)

For a publishable module, add a manifest:

New-ModuleManifest -Path ./MyTools/MyTools.psd1 `
    -RootModule MyTools.psm1 `
    -ModuleVersion "1.0.0" `
    -Author "You" `
    -Description "Personal toolkit" `
    -PowerShellVersion "7.0" `
    -FunctionsToExport @("Get-PublicIP", "Get-DiskFree")

The manifest declares version, dependencies, exported functions, required modules, supported runtime. PowerShellGet needs it to publish.

The PowerShell Gallery

PowerShell Gallery (PSGallery) is the central repository, like npm or PyPI. Use it daily:

Find-Module Az
Install-Module Az -Scope CurrentUser
Update-Module Az
Get-InstalledModule

# Publish your own (with an API key from gallery)
Publish-Module -Path ./MyTools -NuGetApiKey $env:GALLERY_KEY

For private distribution use a private gallery (Azure DevOps Artifacts, GitHub Packages with the NuGet provider) or simply git-clone and copy.

Patterns for Reusability

  • Verb-Noun naming — your functions look like cmdlets and benefit from discovery.
  • One function, one job. Resist God functions; compose smaller ones via the pipeline.
  • Emit objects, not strings. [pscustomobject] makes the next person's job easier.
  • Use Write-Verbose for diagnostic chatter; Write-Information for progress; never Write-Host in scripts (it writes to the host directly, bypassing the pipeline).
  • Handle -WhatIf / -Confirm for destructive functions — declare SupportsShouldProcess = $true and check $PSCmdlet.ShouldProcess(...).

With functions and modules, you can scale from "one-liner I ran once" to "150-function module my team depends on." The next lesson covers the most important production concern — errors, when and how they happen, and how you respond.

Key Takeaways

  • A function is the unit of reuse; an advanced function adds parameter validation, pipeline binding, and help.
  • param(...) blocks declare named, typed, validated parameters.
  • CmdletBinding() upgrades a function to behave like a real cmdlet.
  • Modules (.psm1 + .psd1) package functions for distribution.
  • PSGallery is the central package repo; Install-Module Az is the workhorse command.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →