🎁 New User? Get 20% off your first purchase with code NEWUSER20 Β· ⚑ Instant download Β· πŸ”’ Secure checkout Register Now β†’
Menu

Categories

PowerShell Advanced Functions: CmdletBinding, Parameter Validation, Pipeline Input (2026)

PowerShell Advanced Functions: CmdletBinding, Parameter Validation, Pipeline Input (2026)
PowerShell advanced functions guide

Most of the PowerShell functions in the wild are simple functions β€” a name, a few parameters, a body. They work, but they do not feel like real cmdlets. They do not support -Verbose, they do not support -WhatIf, they accept any garbage as a parameter, and they cannot be used in a pipeline. Turning a simple function into an advanced function takes one attribute and a parameter block β€” but the difference in usability is enormous.

This guide is the practical, end-to-end version: CmdletBinding, parameter validation attributes, pipeline input via ValueFromPipeline and ValueFromPipelineByPropertyName, the begin/process/end blocks, ShouldProcess for safe destructive operations, and the patterns you will use in every module you write. Free PDF cheat sheet at the bottom.

Simple vs advanced functions

Compare:

# Simple function
function Get-DiskFree {
    param($ComputerName)
    Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $ComputerName |
        Select-Object DeviceID, FreeSpace
}

# Advanced function
function Get-DiskFree {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('CN', 'Name')]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName
    )
    process {
        foreach ($c in $ComputerName) {
            Write-Verbose "Querying $c"
            Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $c |
                Select-Object @{n='Server';e={$c}}, DeviceID, FreeSpace
        }
    }
}

The advanced version supports -Verbose, takes pipeline input from a list of names or from objects with a ComputerName / CN / Name property, validates the parameter, and emits one record per disk on every server. The investment is two lines of attributes.

CmdletBinding β€” the magic word

Adding [CmdletBinding()] to the top of param(...) turns a simple function into an advanced one. That single attribute opts you in to all of the common parameters: -Verbose, -Debug, -ErrorAction, -ErrorVariable, -WarningAction, -OutVariable, -OutBuffer, -PipelineVariable. You do not have to write any code for any of them β€” they just work.

Common CmdletBinding options:

[CmdletBinding(
    SupportsShouldProcess = $true,            # enables -WhatIf and -Confirm
    ConfirmImpact = 'High',                   # prompt level (Low / Medium / High)
    DefaultParameterSetName = 'ByName',
    PositionalBinding = $false,
    HelpUri = 'https://dargslan.com/blog/powershell-advanced-functions-cmdletbinding-2026'
)]

The param block

Each parameter is a [Type]$Name declaration with one or more attributes:

param(
    [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
    [string[]]$Name,

    [Parameter()]
    [ValidateRange(1, 65535)]
    [int]$Port = 443,

    [switch]$Force
)

Notes:

  • Always type-annotate parameters. [string[]] means an array of strings; PowerShell will helpfully convert a single string into a one-element array.
  • Default values come right after the parameter name, after all attributes.
  • [switch] is the right type for boolean flags. Test them with if ($Force), never with $Force -eq $true.

Parameter validation attributes

Validation attributes catch bad input before your function body runs. Use them aggressively β€” a function that fails fast with a clear message is much friendlier than one that crashes 50 lines in.

param(
    [ValidateNotNull()]
    [object]$Config,

    [ValidateNotNullOrEmpty()]
    [string]$Path,

    [ValidateLength(1, 64)]
    [string]$Name,

    [ValidateRange(0, 100)]
    [int]$Percent,

    [ValidatePattern('^[a-z][a-z0-9-]*$')]
    [string]$Slug,

    [ValidateSet('Dev', 'Test', 'Prod')]
    [string]$Environment,

    [ValidateScript({ Test-Path $_ })]
    [string]$LogFile,

    [ValidateCount(1, 10)]
    [string[]]$Tags
)

Each one produces a clear error message naming the offending parameter. Use them at the parameter declaration, not inside an if in the body.

Pipeline input

Two ways for a parameter to accept pipeline input:

param(
    # By value: the entire object becomes $Name
    [Parameter(ValueFromPipeline)]
    [string[]]$Name,

    # By property name: extracts the .ComputerName property of each piped object
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]$ComputerName
)

Inside the function, pipeline input is processed one item at a time inside the process block (next section).

begin / process / end blocks

An advanced function with pipeline input has three optional blocks:

function Get-FileSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [string[]]$Path
    )
    begin {
        $totalSize = 0
        $count = 0
    }
    process {
        foreach ($p in $Path) {
            $f = Get-Item $p -ErrorAction SilentlyContinue
            if ($f) {
                $totalSize += $f.Length
                $count++
            }
        }
    }
    end {
        [PSCustomObject]@{
            Files     = $count
            TotalSize = $totalSize
            Average   = if ($count) { [int]($totalSize / $count) } else { 0 }
        }
    }
}

# Use it
Get-ChildItem C:\Logs -Recurse | Get-FileSummary

begin runs once at the start (initialize counters), process runs once per piped object (the work), end runs once at the end (finalize and emit). This is the same lifecycle as a real cmdlet.

ShouldProcess and -WhatIf

For destructive operations (delete, modify, restart), supporting -WhatIf and -Confirm is non-negotiable. Add SupportsShouldProcess to CmdletBinding and gate the dangerous code with $PSCmdlet.ShouldProcess():

function Remove-OldLog {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory)]
        [string]$Path,
        [int]$DaysOld = 30
    )
    Get-ChildItem $Path -File | Where-Object {
        $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld)
    } | ForEach-Object {
        if ($PSCmdlet.ShouldProcess($_.FullName, "Delete file older than $DaysOld days")) {
            Remove-Item $_.FullName
        }
    }
}

# Now it just works:
Remove-OldLog -Path C:\Logs -WhatIf
Remove-OldLog -Path C:\Logs -Confirm

Parameter sets

Sometimes one cmdlet has two or three different ways to be called β€” you have an ID or a name or an object. Parameter sets express this:

function Get-User {
    [CmdletBinding(DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ByName')]
        [string]$Name,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [int]$Id,

        [Parameter(Mandatory, ParameterSetName = 'ByObject', ValueFromPipeline)]
        [pscustomobject]$User
    )
    process {
        switch ($PSCmdlet.ParameterSetName) {
            'ByName'   { ... }
            'ById'     { ... }
            'ByObject' { ... }
        }
    }
}

Comment-based help

Drop a comment block above the function and PowerShell parses it into help that Get-Help can render:

<#
.SYNOPSIS
    Returns disk-free information for one or more computers.
.DESCRIPTION
    Queries Win32_LogicalDisk via CIM. Accepts pipeline input by value
    or by ComputerName property.
.PARAMETER ComputerName
    One or more computer names to query.
.EXAMPLE
    Get-DiskFree -ComputerName web01,web02
.EXAMPLE
    Get-Content servers.txt | Get-DiskFree
.LINK
    https://dargslan.com/blog/powershell-advanced-functions-cmdletbinding-2026
#>
function Get-DiskFree { ... }

Get-Help Get-DiskFree -Full now produces a real help page.

Putting it all together

function Stop-StaleProcess {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High',
                   DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(Mandatory, ValueFromPipeline,
                   ValueFromPipelineByPropertyName,
                   ParameterSetName = 'ByName')]
        [Alias('ProcessName')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Name,

        [Parameter(Mandatory, ParameterSetName = 'ById')]
        [int[]]$Id,

        [ValidateRange(1, 1440)]
        [int]$IdleMinutes = 60,

        [switch]$Force
    )
    begin { $stopped = 0 }
    process {
        $procs = if ($PSCmdlet.ParameterSetName -eq 'ByName') {
            Get-Process -Name $Name -ErrorAction SilentlyContinue
        } else {
            Get-Process -Id $Id -ErrorAction SilentlyContinue
        }
        foreach ($p in $procs) {
            $idle = ((Get-Date) - $p.StartTime).TotalMinutes
            if ($idle -ge $IdleMinutes) {
                $msg = "Stop $($p.Name) (PID $($p.Id), idle $([int]$idle) min)"
                if ($PSCmdlet.ShouldProcess($p.MachineName, $msg)) {
                    Stop-Process -Id $p.Id -Force:$Force
                    $stopped++
                }
            }
        }
    }
    end { Write-Verbose "Stopped $stopped processes" }
}

Read it slowly. Every PowerShell idiom from this article appears exactly once: CmdletBinding with ShouldProcess, two parameter sets, validation, pipeline input, aliases, the three blocks, ShouldProcess gating, and verbose output.

Cheat sheet

All the attributes, syntax, and the full example on a single PDF: PowerShell Advanced Functions Cheat Sheet.

FAQ

Do I always need CmdletBinding?

For one-off scripts, no. For anything that lives in a module or is shared with anyone else, yes. The cost is one line; the benefit is consistent UX with the rest of PowerShell.

What is the difference between $args and a param block?

$args is the unbound positional argument array β€” useful for tiny throwaway functions. The moment you want validation, types, or named parameters, you need a param block.

Can I have both pipeline input and a default value?

Yes. The default value applies when the parameter is not bound from the pipeline or from a positional/named argument.

Can I write my own validation attribute?

Yes β€” derive from System.Management.Automation.ValidateArgumentsAttribute. Useful for organisation-specific patterns (allowed environments, internal hostname formats).

Why is my pipeline input only running once?

You probably forgot the process block. Without it, all pipeline input is dumped into the parameter as a single bound value once at the end, not once per item.

Should every parameter be Mandatory?

No β€” only the ones the function genuinely cannot run without. Optional parameters with sensible defaults make a function more pleasant to use.

How do I make a function callable both as a script and as a module?

Save it as a .ps1 with function Verb-Noun {...} at the top, then Verb-Noun @PSBoundParameters at the bottom. Run as a script for one-shot use, or dot-source / import as a module to expose the function long-term.

Related reading

Share this article:
Dargslan Editorial Team (Dargslan)
About the Author

Dargslan Editorial Team (Dargslan)

Collective of Software Developers, System Administrators, DevOps Engineers, and IT Authors

Dargslan is an independent technology publishing collective formed by experienced software developers, system administrators, and IT specialists.

The Dargslan editorial team works collaboratively to create practical, hands-on technology books focused on real-world use cases. Each publication is developed, reviewed, and...

Programming Languages Linux Administration Web Development Cybersecurity Networking

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.