PowerShell’s true power lies in its extensibility. While built-in cmdlets cover many scenarios, custom cmdlets and advanced functions allow you to create reusable, professional-grade tools tailored to your specific needs. This comprehensive guide explores how to build custom cmdlets and advanced functions that integrate seamlessly with PowerShell’s ecosystem.
Understanding the Foundation
Before diving into creation, it’s essential to understand the distinction between functions and cmdlets. Functions are PowerShell script blocks that can accept parameters and return output. Advanced functions extend this concept by adding cmdlet-like behavior through parameter attributes and automatic features.
Simple Functions vs Advanced Functions
Simple functions provide basic functionality without the overhead of cmdlet features. They’re perfect for straightforward tasks but lack advanced parameter processing and pipeline integration.
# Simple Function
function Get-SimpleGreeting {
param($Name)
"Hello, $Name!"
}
Get-SimpleGreeting -Name "Alex"
Output:
Hello, Alex!
Creating Advanced Functions with CmdletBinding
Advanced functions transform ordinary functions into cmdlet-like tools by adding the [CmdletBinding()] attribute. This single addition unlocks automatic features like common parameters, parameter validation, and improved error handling.
function Get-AdvancedGreeting {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[string]$Title = "Friend"
)
Write-Verbose "Processing greeting for $Name"
"Hello, $Title $Name!"
}
# Usage with common parameters
Get-AdvancedGreeting -Name "Alex" -Title "Dr." -Verbose
Output:
VERBOSE: Processing greeting for Alex
Hello, Dr. Alex!
Parameter Attributes and Validation
Parameter attributes provide declarative control over how parameters behave. They enforce validation rules, define positions, and control pipeline input without writing conditional logic.
function New-UserAccount {
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateLength(3,20)]
[string]$Username,
[Parameter(Mandatory=$true)]
[ValidateSet("Admin", "User", "Guest")]
[string]$Role,
[Parameter(Mandatory=$false)]
[ValidateRange(18,100)]
[int]$Age,
[Parameter(Mandatory=$false)]
[ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
[string]$Email
)
if ($PSCmdlet.ShouldProcess($Username, "Create user account")) {
Write-Output "Creating account for $Username with role: $Role"
if ($Age) {
Write-Output "Age: $Age"
}
if ($Email) {
Write-Output "Email: $Email"
}
}
}
# Valid usage
New-UserAccount -Username "jdoe" -Role "User" -Age 25 -Email "[email protected]"
Output:
Creating account for jdoe with role: User
Age: 25
Email: [email protected]
Pipeline Support and ValueFromPipeline
Pipeline integration is a hallmark of PowerShell cmdlets. Advanced functions support pipeline input through the ValueFromPipeline and ValueFromPipelineByPropertyName attributes, combined with Begin, Process, and End blocks.
function Get-FileSize {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
[string[]]$Path
)
begin {
Write-Verbose "Starting file size calculation"
$totalSize = 0
$fileCount = 0
}
process {
foreach ($file in $Path) {
if (Test-Path $file -PathType Leaf) {
$item = Get-Item $file
$sizeInKB = [math]::Round($item.Length / 1KB, 2)
[PSCustomObject]@{
FileName = $item.Name
SizeKB = $sizeInKB
LastModified = $item.LastWriteTime
}
$totalSize += $item.Length
$fileCount++
}
}
}
end {
Write-Verbose "Processed $fileCount files"
$totalKB = [math]::Round($totalSize / 1KB, 2)
Write-Host "`nTotal: $totalKB KB across $fileCount files" -ForegroundColor Green
}
}
# Pipeline usage
Get-ChildItem "C:\Temp" -File | Get-FileSize -Verbose
Sample Output:
VERBOSE: Starting file size calculation
FileName SizeKB LastModified
-------- ------ ------------
document.txt 15.23 10/20/2025 2:30:00 PM
data.csv 142.87 10/21/2025 9:15:00 AM
notes.md 3.45 10/22/2025 11:00:00 AM
VERBOSE: Processed 3 files
Total: 161.55 KB across 3 files
Parameter Sets for Flexible Interfaces
Parameter sets allow a single function to support multiple usage patterns. They define mutually exclusive parameter combinations, making your cmdlets more versatile while maintaining clarity.
function Connect-ServiceAccount {
[CmdletBinding(DefaultParameterSetName="Credential")]
param(
[Parameter(Mandatory=$true, ParameterSetName="Credential")]
[PSCredential]$Credential,
[Parameter(Mandatory=$true, ParameterSetName="Token")]
[string]$AccessToken,
[Parameter(Mandatory=$true, ParameterSetName="Certificate")]
[string]$CertificateThumbprint,
[Parameter(Mandatory=$true)]
[string]$ServiceUrl
)
$authMethod = $PSCmdlet.ParameterSetName
switch ($authMethod) {
"Credential" {
Write-Output "Connecting to $ServiceUrl with username: $($Credential.UserName)"
}
"Token" {
Write-Output "Connecting to $ServiceUrl with access token"
}
"Certificate" {
Write-Output "Connecting to $ServiceUrl with certificate: $CertificateThumbprint"
}
}
}
# Different authentication methods
Connect-ServiceAccount -ServiceUrl "https://api.example.com" -Credential (Get-Credential)
Connect-ServiceAccount -ServiceUrl "https://api.example.com" -AccessToken "abc123xyz"
Error Handling and Terminating vs Non-Terminating Errors
Professional cmdlets handle errors gracefully. PowerShell distinguishes between terminating errors (which stop execution) and non-terminating errors (which allow continued processing).
function Test-NetworkConnection {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string[]]$ComputerName,
[Parameter()]
[int]$TimeoutMs = 1000
)
process {
foreach ($computer in $ComputerName) {
try {
Write-Verbose "Testing connection to $computer"
if ([string]::IsNullOrWhiteSpace($computer)) {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
[System.ArgumentException]::new("Computer name cannot be empty"),
"EmptyComputerName",
[System.Management.Automation.ErrorCategory]::InvalidArgument,
$computer
)
$PSCmdlet.WriteError($errorRecord)
continue
}
$ping = Test-Connection -ComputerName $computer -Count 1 -TimeoutSeconds ($TimeoutMs/1000) -ErrorAction Stop
[PSCustomObject]@{
ComputerName = $computer
Status = "Online"
ResponseTime = $ping.ResponseTime
IPAddress = $ping.Address
}
}
catch {
Write-Warning "Failed to connect to $computer: $($_.Exception.Message)"
[PSCustomObject]@{
ComputerName = $computer
Status = "Offline"
ResponseTime = $null
IPAddress = $null
}
}
}
}
}
# Usage
"localhost", "invalid-host", "127.0.0.1" | Test-NetworkConnection -Verbose
Output Types and Custom Objects
Defining output types improves discoverability and enables IntelliSense. Custom objects provide structured, consistent output that integrates seamlessly with PowerShell’s pipeline and formatting system.
function Get-ProcessInfo {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
[Parameter(Mandatory=$false)]
[string]$Name = "*",
[Parameter()]
[switch]$IncludeModules
)
$processes = Get-Process -Name $Name -ErrorAction SilentlyContinue
foreach ($proc in $processes) {
$output = [PSCustomObject]@{
PSTypeName = "CodeLucky.ProcessInfo"
ProcessName = $proc.Name
ProcessId = $proc.Id
MemoryMB = [math]::Round($proc.WorkingSet64 / 1MB, 2)
CPUSeconds = [math]::Round($proc.CPU, 2)
StartTime = $proc.StartTime
Responding = $proc.Responding
}
if ($IncludeModules) {
$output | Add-Member -NotePropertyName "ModuleCount" -NotePropertyValue $proc.Modules.Count
}
$output
}
}
# Usage
Get-ProcessInfo -Name "powershell" -IncludeModules | Format-Table -AutoSize
Sample Output:
ProcessName ProcessId MemoryMB CPUSeconds StartTime Responding ModuleCount
----------- --------- -------- ---------- --------- ---------- -----------
powershell 8456 145.23 12.45 10/22/2025 1:30:00 PM True 89
powershell 12304 98.76 5.32 10/22/2025 2:15:00 PM True 85
Dynamic Parameters for Context-Sensitive Behavior
Dynamic parameters adapt based on other parameter values or runtime context. They provide sophisticated interfaces that respond to user input, similar to how Get-ChildItem exposes different parameters based on the provider.
function Get-DataSource {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateSet("SQL", "API", "File")]
[string]$SourceType
)
DynamicParam {
$paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
if ($SourceType -eq "SQL") {
$serverParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
"ServerName",
[string],
(New-Object System.Collections.ObjectModel.Collection[System.Attribute])
)
$serverParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
$paramDictionary.Add("ServerName", $serverParam)
$databaseParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
"DatabaseName",
[string],
(New-Object System.Collections.ObjectModel.Collection[System.Attribute])
)
$databaseParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
$paramDictionary.Add("DatabaseName", $databaseParam)
}
elseif ($SourceType -eq "API") {
$urlParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
"ApiUrl",
[string],
(New-Object System.Collections.ObjectModel.Collection[System.Attribute])
)
$urlParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
$paramDictionary.Add("ApiUrl", $urlParam)
}
elseif ($SourceType -eq "File") {
$pathParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
"FilePath",
[string],
(New-Object System.Collections.ObjectModel.Collection[System.Attribute])
)
$pathParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
$paramDictionary.Add("FilePath", $pathParam)
}
return $paramDictionary
}
process {
Write-Output "Connecting to $SourceType source"
switch ($SourceType) {
"SQL" {
Write-Output "Server: $($PSBoundParameters['ServerName'])"
Write-Output "Database: $($PSBoundParameters['DatabaseName'])"
}
"API" {
Write-Output "API URL: $($PSBoundParameters['ApiUrl'])"
}
"File" {
Write-Output "File Path: $($PSBoundParameters['FilePath'])"
}
}
}
}
# Usage - different parameters appear based on SourceType
Get-DataSource -SourceType SQL -ServerName "SQLSERVER01" -DatabaseName "Production"
Get-DataSource -SourceType API -ApiUrl "https://api.example.com/v1"
Compiled Cmdlets with C#
For maximum performance and integration with .NET libraries, compiled cmdlets written in C# provide the ultimate extensibility. They require more setup but offer superior performance and full access to the .NET ecosystem.
// Save as Get-MathOperation.cs
using System;
using System.Management.Automation;
namespace CodeLucky.PowerShell
{
[Cmdlet(VerbsCommon.Get, "MathOperation")]
[OutputType(typeof(double))]
public class GetMathOperationCommand : PSCmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public double FirstNumber { get; set; }
[Parameter(Mandatory = true, Position = 1)]
public double SecondNumber { get; set; }
[Parameter(Mandatory = true)]
[ValidateSet("Add", "Subtract", "Multiply", "Divide")]
public string Operation { get; set; }
protected override void ProcessRecord()
{
double result = 0;
switch (Operation)
{
case "Add":
result = FirstNumber + SecondNumber;
break;
case "Subtract":
result = FirstNumber - SecondNumber;
break;
case "Multiply":
result = FirstNumber * SecondNumber;
break;
case "Divide":
if (SecondNumber == 0)
{
ThrowTerminatingError(new ErrorRecord(
new DivideByZeroException("Cannot divide by zero"),
"DivisionByZero",
ErrorCategory.InvalidArgument,
SecondNumber));
return;
}
result = FirstNumber / SecondNumber;
break;
}
WriteObject(result);
}
}
}
Compilation and Usage:
# Compile the cmdlet
Add-Type -Path "Get-MathOperation.cs" -ReferencedAssemblies System.Management.Automation
# Use the compiled cmdlet
Get-MathOperation -FirstNumber 10 -SecondNumber 5 -Operation Multiply
Output:
50
Best Practices for Custom Cmdlets
Naming Conventions
Follow PowerShell’s verb-noun naming pattern using approved verbs from Get-Verb. This ensures consistency and predictability across the PowerShell ecosystem.
# Good naming
Get-UserProfile
New-DatabaseConnection
Set-ConfigurationValue
Remove-TemporaryFile
# Poor naming
Fetch-User # Use Get instead of Fetch
CreateDB # Use New and full noun
UpdateConfig # Use Set
DeleteTemp # Use Remove
Parameter Design
Design parameters with usability in mind. Support pipeline input where logical, provide sensible defaults, and use parameter sets to avoid confusion.
function Export-DataReport {
[CmdletBinding(DefaultParameterSetName="CSV")]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[PSCustomObject[]]$Data,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[Parameter(ParameterSetName="CSV")]
[switch]$AsCSV,
[Parameter(ParameterSetName="JSON")]
[switch]$AsJSON,
[Parameter(ParameterSetName="XML")]
[switch]$AsXML,
[Parameter()]
[switch]$Force
)
begin {
$items = @()
}
process {
$items += $Data
}
end {
if ((Test-Path $OutputPath) -and -not $Force) {
throw "Output file exists. Use -Force to overwrite."
}
switch ($PSCmdlet.ParameterSetName) {
"CSV" { $items | Export-Csv -Path $OutputPath -NoTypeInformation }
"JSON" { $items | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath }
"XML" { $items | Export-Clixml -Path $OutputPath }
}
Write-Output "Exported $($items.Count) items to $OutputPath"
}
}
Documentation with Comment-Based Help
Comprehensive help documentation makes your cmdlets accessible to other users. Comment-based help integrates with Get-Help and provides examples, descriptions, and parameter documentation.
function Get-SystemReport {
<#
.SYNOPSIS
Generates a comprehensive system report.
.DESCRIPTION
The Get-SystemReport cmdlet collects system information including CPU, memory,
disk space, and running services. It supports filtering by report type and
exporting results to various formats.
.PARAMETER ReportType
Specifies the type of report to generate. Valid values are Hardware, Software,
Network, and All. Default is All.
.PARAMETER ComputerName
Specifies the computer name to query. Defaults to the local computer.
.PARAMETER ExportPath
Optional path to export the report. Supports CSV, JSON, and HTML formats
based on file extension.
.EXAMPLE
Get-SystemReport
Generates a complete system report for the local computer.
.EXAMPLE
Get-SystemReport -ReportType Hardware -ComputerName SERVER01
Generates a hardware-only report for the remote computer SERVER01.
.EXAMPLE
Get-SystemReport -ExportPath "C:\Reports\system-report.html"
Generates a report and exports it to an HTML file.
.INPUTS
String
You can pipe computer names to Get-SystemReport.
.OUTPUTS
PSCustomObject
Returns custom objects containing system information.
.NOTES
Author: CodeLucky.com
Version: 1.0.0
Requires: PowerShell 5.1 or later
.LINK
https://codelucky.com/powershell/system-reports
#>
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet("Hardware", "Software", "Network", "All")]
[string]$ReportType = "All",
[Parameter(ValueFromPipeline=$true)]
[string]$ComputerName = $env:COMPUTERNAME,
[Parameter()]
[string]$ExportPath
)
# Implementation here
Write-Output "Generating $ReportType report for $ComputerName"
}
# Access help
Get-Help Get-SystemReport -Full
Get-Help Get-SystemReport -Examples
Testing and Debugging Custom Cmdlets
Robust testing ensures your cmdlets behave correctly across various scenarios. Use Pester for unit testing and leverage PowerShell’s debugging capabilities.
# Pester test example (Save as Get-UserProfile.Tests.ps1)
BeforeAll {
. "$PSScriptRoot\Get-UserProfile.ps1"
}
Describe "Get-UserProfile" {
Context "Parameter Validation" {
It "Should require Username parameter" {
{ Get-UserProfile } | Should -Throw
}
It "Should accept valid username" {
{ Get-UserProfile -Username "testuser" } | Should -Not -Throw
}
}
Context "Output Validation" {
It "Should return PSCustomObject" {
$result = Get-UserProfile -Username "testuser"
$result | Should -BeOfType [PSCustomObject]
}
It "Should include required properties" {
$result = Get-UserProfile -Username "testuser"
$result.PSObject.Properties.Name | Should -Contain "Username"
$result.PSObject.Properties.Name | Should -Contain "ProfilePath"
}
}
Context "Pipeline Support" {
It "Should accept pipeline input" {
{ "user1", "user2" | Get-UserProfile } | Should -Not -Throw
}
}
}
# Run tests
Invoke-Pester -Path "Get-UserProfile.Tests.ps1"
Performance Optimization
Optimize your cmdlets for performance by minimizing object creation, using efficient collection types, and avoiding unnecessary iterations.
function Get-LargeDataSet {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[int]$ItemCount
)
begin {
# Use ArrayList for better performance with large collections
$results = [System.Collections.ArrayList]::new($ItemCount)
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
}
process {
for ($i = 1; $i -le $ItemCount; $i++) {
# Avoid += operator with arrays (creates new array each time)
[void]$results.Add([PSCustomObject]@{
Id = $i
Name = "Item$i"
Value = Get-Random -Minimum 1 -Maximum 1000
})
# Progress reporting for long operations
if ($i % 1000 -eq 0) {
Write-Progress -Activity "Generating data" -Status "$i of $ItemCount" -PercentComplete (($i / $ItemCount) * 100)
}
}
}
end {
Write-Progress -Activity "Generating data" -Completed
$stopwatch.Stop()
Write-Verbose "Generated $ItemCount items in $($stopwatch.ElapsedMilliseconds)ms"
# Return as array for pipeline compatibility
, $results.ToArray()
}
}
# Usage
$data = Get-LargeDataSet -ItemCount 10000 -Verbose
Conclusion
Creating custom cmdlets and advanced functions transforms you from a PowerShell user into a PowerShell developer. By mastering parameter binding, pipeline support, error handling, and best practices, you can build professional-grade tools that integrate seamlessly with PowerShell’s ecosystem. Whether you choose advanced functions for flexibility or compiled cmdlets for performance, the principles remain consistent: follow conventions, prioritize usability, and leverage PowerShell’s powerful features.
Start with simple advanced functions, gradually incorporate more sophisticated features like dynamic parameters and custom formatting, and test thoroughly. Your custom cmdlets will become invaluable tools in your automation arsenal, enabling you to tackle complex scenarios with elegant, reusable solutions.








