Advanced Functions: CmdletBinding, Parameter Validation & Pipeline Input

PowerShell Intermediate/Advanced

Chapter 1 · Advanced Functions: CmdletBinding, Parameter Validation & Pipeline Input

PowerShell Fundamentals covered functions honestly, but deliberately kept them simple: a param() block, a type, maybe [Parameter(Mandatory)], and — as the capstone's own Get-OldFileReport showed — real discipline about never letting stray output pollute the return value. That's genuinely enough for scripts you write and run yourself. This course exists for the next step: functions built to behave like real, first-class cmdlets — with automatic parameters, real input validation, native pipeline support, and the same -WhatIf safety net Remove-Item itself has. It starts with the one attribute that unlocks all of it.

[CmdletBinding()]: Turning a Function Into a Real Cmdlet

Adding [CmdletBinding()] immediately after function Name { — before param() — promotes a plain function into what PowerShell calls an advanced function, and the built-in cmdlets used throughout Fundamentals have always secretly worked this way:

function Get-Square { [CmdletBinding()] param( [Parameter(Mandatory)] [int]$n ) $n * $n }

That one attribute adds a whole family of CommonParameters automatically — -Verbose, -Debug, -ErrorAction, -WarningAction, -ErrorVariable, -WarningVariable, -OutVariable, -OutBuffer, and -PipelineVariable — with zero extra param() entries needed:

(Get-Command Get-Square).Parameters.Keys # n # Verbose # Debug # ErrorAction # WarningAction # ErrorVariable # ...and the rest, all for free

This is exactly what made -Verbose and -ErrorAction Stop feel like built-in language features throughout Fundamentals — every cmdlet you called there had [CmdletBinding()] (or its C#-level equivalent) applied the whole time.

Parameter Validation Attributes

Fundamentals' own [Parameter(Mandatory)] only checked that a value was supplied at all. Validation attributes check that the value supplied is actually acceptable, rejecting bad input before the function body ever runs:

AttributeChecks
[ValidateNotNullOrEmpty()]Rejects $null or an empty string/collection
[ValidateRange(1, 100)]Numeric value must fall within the given range
[ValidateSet("Low","Medium","High")]Value must be one of a fixed, explicit list
[ValidatePattern('^\d{5}$')]Value must match a regular expression
[ValidateScript({ Test-Path $_ })]Value must pass an arbitrary custom script block
[ValidateCount(1, 5)]An array parameter must have between the given number of elements
function Set-Priority { [CmdletBinding()] param( [ValidateSet("Low", "Medium", "High")] [string]$Level, [ValidateRange(1, 100)] [int]$Percent ) "$Level at $Percent%" } Set-Priority -Level "Urgent" -Percent 50 # error, immediately — "Urgent" isn't in the ValidateSet list; the function body never even runs

Pipeline Input: ValueFromPipeline vs. ValueFromPipelineByPropertyName

A Fundamentals-style function only ever received arguments passed explicitly by name or position. An advanced function can accept objects piped directly in, in two genuinely different ways:

function Test-ByWholeObject { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string]$Name # the ENTIRE piped-in value becomes $Name ) process { "Got: $Name" } } "Alice", "Bob" | Test-ByWholeObject function Test-ByProperty { [CmdletBinding()] param( [Parameter(ValueFromPipelineByPropertyName)] [string]$ProcessName # matched by PROPERTY NAME on the incoming object ) process { "Got: $ProcessName" } } Get-Process | Test-ByProperty # works because Get-Process objects have a real ProcessName property

ValueFromPipeline is right when the whole piped-in object is the value you want. ValueFromPipelineByPropertyName is right when you're receiving richer objects (like Chapter 3's own Get-Process output) and only want to pull one matching property off each one — exactly the kind of binding that makes so many built-in cmdlets pipe together without you ever manually extracting a property first.

Why Pipeline-Bound Functions Need a process {} Block

# WITHOUT process{} — a real, common bug function Show-NameBroken { [CmdletBinding()] param([Parameter(ValueFromPipeline)][string]$Name) "Name is: $Name" # NOT inside process{} } "Alice", "Bob", "Carol" | Show-NameBroken # Name is: Carol — only ONE line, only the LAST object # WITH process{} — correct function Show-NameFixed { [CmdletBinding()] param([Parameter(ValueFromPipeline)][string]$Name) process { "Name is: $Name" } } "Alice", "Bob", "Carol" | Show-NameFixed # Name is: Alice # Name is: Bob # Name is: Carol
The central fact this chapter is built on
A function body written as one plain block — with no begin/process/end — runs exactly once, no matter how many objects the pipeline sends it, and by the time that single run executes, a pipeline-bound parameter only still holds the last object received. begin {} runs once, before the first pipeline object arrives — the right place for one-time setup. process {} runs once per pipeline object — the only block that actually sees every item. end {} runs once, after the last object has been processed — the right place for a final summary. Fundamentals' own functions never needed this distinction because none of them declared ValueFromPipeline — the moment a parameter does, process {} stops being optional.

SupportsShouldProcess: Real -WhatIf/-Confirm Support

The Fundamentals capstone leaned on Remove-Item's own built-in safety — this is how to give a function you write that exact same behavior:

function Remove-TempFile { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline)] [string]$Path ) process { if ($PSCmdlet.ShouldProcess($Path, "Delete")) { Remove-Item -Path $Path -ErrorAction Stop } } } Remove-TempFile -Path "C:\temp\old.log" -WhatIf # What if: Performing the operation "Delete" on target "C:\temp\old.log". # Nothing is actually deleted — exactly like calling Remove-Item itself with -WhatIf

$PSCmdlet.ShouldProcess() is only meaningful once SupportsShouldProcess is declared — it automatically wires up both -WhatIf (preview only, shown above) and -Confirm (an interactive yes/no prompt before each action), with zero extra parameter declarations of your own.

A first practical habit
Any function you write that deletes, overwrites, or otherwise changes something irreversibly is worth reaching for [CmdletBinding(SupportsShouldProcess)] on by default — the same instinct that made Fundamentals' own capstone wrap Remove-Item in try/catch, just built into the function itself this time.
A missing process{} block fails silently, not loudly
Show-NameBroken above didn't throw an error — it just quietly processed one object instead of three, with no warning that anything was wrong. This is exactly the kind of bug that survives a quick manual test (where you might only ever pipe in one item) and then breaks the moment it meets a real, multi-item pipeline in production. Any parameter marked ValueFromPipeline or ValueFromPipelineByPropertyName is a signal to double-check that a process {} block actually exists.

Hands-On Exercises

Exercise 1

Explain why Show-NameBroken only prints one line ("Carol") when three names are piped into it, using this chapter's own explanation of how a plain function body executes versus a process {} block.

📄 View solution
Exercise 2

Explain the difference between ValueFromPipeline and ValueFromPipelineByPropertyName. Using this chapter's own Get-Process | Test-ByProperty example, explain specifically why ValueFromPipeline wouldn't have worked correctly there instead.

📄 View solution
Exercise 3

Write a function Remove-OldLog that accepts a mandatory, pipeline-bound [string]$Path, supports -WhatIf/-Confirm via SupportsShouldProcess, and correctly processes every object piped into it (not just the last one). Explain how your function avoids both gotchas covered in this chapter.

📄 View solution

Chapter 1 Quick Reference

  • [CmdletBinding()] — promotes a plain function to an advanced function; unlocks CommonParameters (-Verbose, -Debug, -ErrorAction, etc.) automatically
  • Validation attributes[ValidateNotNullOrEmpty()] [ValidateRange()] [ValidateSet()] [ValidatePattern()] [ValidateScript()] [ValidateCount()], checked before the function body ever runs
  • ValueFromPipeline — the entire piped object becomes the parameter's value
  • ValueFromPipelineByPropertyName — binds by matching a property name on the incoming object to the parameter name
  • begin {} / process {} / end {} — once before / once per pipeline object / once after; a pipeline-bound parameter needs process {} or it silently only sees the last item
  • [CmdletBinding(SupportsShouldProcess)] + $PSCmdlet.ShouldProcess() — real, built-in -WhatIf/-Confirm support, the same safety Remove-Item itself has
  • Next chapter: Regular Expressions & Advanced String/Text Processing