PowerShell Splatting: Advanced Patterns for Cleaner Scripts

PowerShell Splatting: Advanced Patterns for Cleaner Scripts

PowerShell Tips Editor 5 min read
PowerShell Splatting: Advanced Patterns for Cleaner Scripts

Why Advanced Splatting Changes Everything

Most PowerShell users discover splatting when a command grows too wide to read comfortably. That is a fine starting point, but it barely scratches the surface. Once you start writing wrapper functions, proxy commands, or any script that passes parameters dynamically based on runtime conditions, splatting transforms from a readability trick into the foundation of clean, maintainable code. This post covers the patterns that move splatting from cosmetic to structural.

Quick Answer

Build a hashtable named $params, add or remove keys conditionally, then call your command with @params. Inside wrapper functions, start from $PSBoundParameters and remove keys your wrapper consumes before passing the rest through to the inner command.

Basic Splatting Recap with Hashtable and Array Forms

PowerShell supports two splatting forms. The hashtable form maps parameter names to values and works with named parameters. The array form passes positional arguments in order. The hashtable form is almost always preferred because it is self-documenting and order-independent.

# Hashtable form — named parameters
$copyParams = @{
    Path        = 'C:\Source\report.csv'
    Destination = 'D:\Archive\report.csv'
    Force       = $true
    ErrorAction = 'Stop'
}
Copy-Item @copyParams

# Array form — positional parameters only
$positional = @('C:\Source\report.csv', 'D:\Archive\report.csv')
Copy-Item @positional

Notice the @ sigil instead of $ at the call site. The variable itself is still accessed as $copyParams; the @ is the splat operator. The two forms cannot be mixed in a single splat, but you can combine a hashtable splat with individually specified parameters in the same call.

Building a Splat Dynamically from Conditional Logic

The real power emerges when you build the hashtable at runtime. Conditional parameter addition keeps command calls clean while letting logic vary what gets bound. This is far cleaner than building a long string of if blocks each with a full command call.

function Invoke-Deploy {
    param(
        [string]$Target,
        [string]$Credential,
        [switch]$WhatIf,
        [int]$RetryCount = 3
    )

    $deployParams = @{
        Target     = $Target
        RetryCount = $RetryCount
    }

    # Only add credential key when one is supplied
    if ($Credential) {
        $deployParams['Credential'] = $Credential
    }

    # WhatIf is a switch — only add when $true
    if ($WhatIf) {
        $deployParams['WhatIf'] = $true
    }

    Start-DeploymentJob @deployParams
}

Key rule: adding a key with a $null value still binds that parameter. Use $deployParams.Remove('Key') or an if guard to prevent unintended bindings.

Using PSBoundParameters to Pass Through Parameters

$PSBoundParameters is an automatic hashtable containing every parameter the caller actually passed to the current function — not defaults, only explicit values. This makes it ideal for wrapper functions that want to forward everything to an inner command without manually listing every parameter.

function Copy-ItemSafe {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][string]$Destination,
        [switch]$Force
    )

    # $PSBoundParameters already contains exactly what the caller passed
    # Just splat it straight through
    try {
        Copy-Item @PSBoundParameters -ErrorAction Stop
        Write-Verbose "Copied '$Path' to '$Destination'"
    }
    catch {
        Write-Error "Copy failed: $_"
    }
}

Because $PSBoundParameters only contains bound values, optional parameters the caller omitted are automatically absent from the splat — no manual key removal needed for them.

Combining PSBoundParameters with Extra Keys

Wrapper functions often need to consume some of their own parameters and add new ones before passing through to the inner command. The safest pattern is to copy $PSBoundParameters into a new hashtable, remove wrapper-specific keys, and add inner-command keys as needed.

function Write-LoggedOutput {
    [CmdletBinding()]
    param(
        [string]$LogPath,           # wrapper-only param
        [string]$Message,
        [string]$ForegroundColor = 'White'
    )

    # Clone to avoid mutating the automatic variable
    $innerParams = @{} + $PSBoundParameters

    # Remove our wrapper-specific key before passing through
    $innerParams.Remove('LogPath')

    # Add an inner-command-specific key
    $innerParams['NoNewline'] = $false

    Write-Host @innerParams

    if ($LogPath) {
        Add-Content -Path $LogPath -Value $Message
    }
}

Splatting Arrays for Positional Parameters

Array splatting is useful when calling external programs or cmdlets that primarily use positional parameters. Build an array of arguments in order and splat with @. This keeps complex argument lists readable and programmatically modifiable without string concatenation.

$robocopyArgs = @(
    'C:\Source'
    'D:\Backup'
    '/MIR'
    '/R:3'
    '/W:5'
    '/LOG:C:\Logs\robocopy.log'
)
robocopy @robocopyArgs

Debugging: What -Verbose Shows About Splatted Calls

When you add -Verbose to a splatted call, PowerShell expands the hashtable in the verbose stream exactly as if you had typed each parameter individually. This means -Verbose output is a reliable way to confirm which parameters were actually bound. Alternatively, set $DebugPreference = 'Continue' and add a Write-Debug ($params | Out-String) line before the splatted call during development.

Common Errors

  • Using @ when you mean $ in the variable definition. The splat operator @params is only valid at a call site. Writing @params = @{...} causes a parse error; the assignment always uses $params = @{...}.
  • Passing a key with a $null value still binds the parameter. If you build a hashtable and set a key to $null thinking it will be ignored, PowerShell will bind that parameter with a null value. Use $params.Remove('KeyName') explicitly to exclude a key, or guard the assignment with an if check.

Related Cmdlets / See Also

Wrapping Up

Advanced splatting — dynamic hashtable construction, $PSBoundParameters passthrough, and per-key guards — eliminates repetitive command calls and makes wrapper functions genuinely reusable. Master these patterns and your functions will handle parameter variation cleanly without a wall of if blocks surrounding every command invocation.

Send-Item -To