PowerShell Splatting: Explicit Parameter Sets and Safe Forwarding
Use PowerShell splatting to construct readable commands, preserve typed values, forward bound parameters selectively, and avoid accidental overrides.
Splatting lets PowerShell pass a collection of values to a command as its parameters. It is particularly valuable when a command has optional flags, when parameters are assembled conditionally, or when a wrapper function forwards a carefully chosen subset of its own inputs. The @ splat syntax does not stringify the collection first: values remain PowerShell objects until the command’s parameter binder processes them.
The two main forms are a hashtable for named parameters and an array for positional parameters. Named hashtable splatting is usually easier to review because the command intent stays connected to parameter names. Array splatting can be useful when an interface is explicitly positional, but it is more sensitive to parameter-order changes and less self-documenting.
Prefer named splatting for maintainable commands
A hashtable maps parameter names to values. Use keys without the leading hyphen, and represent switch parameters with $true or $false.
$copyOptions = @{
LiteralPath = './input/report final.csv'
Destination = './archive/report.csv'
PassThru = $true
}
Copy-Item @copyOptions
This is particularly useful when building options based on configuration. It avoids a long command line with embedded conditional expressions and gives reviewers a stable place to inspect every configured value.
$options = @{
Path = './src'
Recurse = $true
File = $true
}
if ($IncludeHidden) {
$options.Force = $true
}
Get-ChildItem @options
Only include keys that the target command supports. A generic hashtable assembled from unvalidated external input can fail at binding time or choose an unintended parameter if a value is malformed. Treat a splat as a structured call interface: construct it from trusted keys in code, validate its values, and inspect Get-Command -Syntax when module versions change.
You can combine explicit parameters and one or more splatted collections, but avoid duplicate keys unless the command’s precedence behavior is intentional and tested. PowerShell 7.1 and later allow an explicitly named parameter to override a value from a splat. That convenience can hide a stale setting in a shared hashtable, so for configuration-sensitive code it is often clearer to produce a final options object with the intended value already resolved.
Arrays are for position-dependent interfaces
An array splat supplies values by positional parameter order. This is compact, but a command’s positional metadata is part of the contract. If an upstream cmdlet changes parameter order or a reader cannot remember which value occupies position 1, the array is difficult to review.
$positional = @('./input/report final.csv', './archive/report.csv')
Copy-Item @positional -WhatIf
Use named hashtables for public automation whenever possible. Reserve array splatting for cases where positional semantics are explicitly stable, such as passing a simple argument vector to a wrapper you own. A splatted array for a PowerShell cmdlet still goes through PowerShell parameter binding; it is not the same as constructing a raw native process command line.
Splatting also does not flatten complex values just because the @ character is used. If a parameter expects a collection, pass a collection. If it expects one object, pass one object. Confirm array shape at the receiving boundary, particularly for a single-element array that may be enumerated when produced from a pipeline.
Forward parameters without leaking the whole caller interface
$PSBoundParameters contains the named parameters that were explicitly bound to the current function or script. Splatting it can reduce duplication when forwarding a transparent wrapper to another command with a matching interface:
function Get-FilteredProcess {
[CmdletBinding()]
param(
[string] $Name,
[int] $Id
)
Get-Process @PSBoundParameters
}
This pattern is appropriate only when the downstream command accepts the same parameter names and meanings. If the wrapper’s public API differs, forwarding every bound parameter can cause errors or pass a value with different semantics. Build a new hashtable that deliberately maps the wrapper’s contract to the callee’s contract:
function Find-ProcessRecord {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $ProcessName,
[Parameter()]
[switch] $IncludePath
)
$query = @{ Name = $ProcessName }
$processes = Get-Process @query
if ($IncludePath) {
$processes | Select-Object Name, Id, Path
}
else {
$processes | Select-Object Name, Id
}
}
An explicitly constructed splat is more verbose than @PSBoundParameters, but it is safer when a wrapper is an abstraction boundary. It prevents internal control flags, common parameters, or future additions from accidentally leaking into a downstream call. If you do forward $PSBoundParameters, exclude keys that the callee should not receive and document the intended pass-through behavior.
Keep types and switches intentional
Hashtable values can be strings, numbers, booleans, arrays, script blocks, or other objects. Do not quote a boolean switch as the text 'false'; a non-empty string can convert to $true and produce a surprising call. Prefer actual typed values and validate configuration while it is loaded.
$options = @{
Recurse = [bool]$settings.Recurse
Depth = [int]$settings.Depth
}
if (-not $options.Recurse) {
$options.Remove('Recurse')
}
Get-ChildItem @options
Whether an explicit false switch should be passed or omitted depends on the target command’s semantics. Some parameters are switch flags that only turn behavior on; others are Boolean-valued parameters that accept either state. Consult Get-Help Command -Parameter Name, including the parameter type and default. Don’t infer the contract from a parameter’s English name.
For native executables, a PowerShell hashtable cannot be splatted as named PowerShell parameters unless the command is a PowerShell command that understands those names. Use an argument array for native invocations and validate how the current PowerShell version serializes the values, especially empty strings and embedded quotes. A structured splat is not proof that the native program received the intended argument vector.
Debug and test the final command shape
Inspect hashtable keys and types before invocation:
$options.GetEnumerator() |
Sort-Object -Property Name |
ForEach-Object {
'{0}: {1}' -f $_.Key, $_.Value.GetType().FullName
}
That diagnostic assumes values are non-null; guard nulls in real troubleshooting code. Avoid writing argument values to logs if they can contain credentials or personal data. If parameter binding fails, compare the hashtable keys with the target command’s actual parameters and run a minimal invocation with -WhatIf where supported.
Tests should cover the case where an optional parameter is omitted, explicitly false, true, null, empty, and invalid. Verify wrapper forwarding with a mock command or Pester mock rather than contacting a production endpoint. If a function supports -WhatIf, assert that no mutation occurs in that mode and that the final splat still contains the intended destination and operation. Splatting makes command construction easier to see, but the command’s parameter contract remains the authority.
Define precedence before combining option sources
Production scripts often combine defaults, a configuration file, and a small number of explicit command-line overrides. Hashtable splatting makes that composition visible, but only if precedence is intentional. Start with defaults, apply validated configuration, then apply explicit caller overrides. Avoid silently merging arbitrary hashtables from different sources because a later key can replace a safety-sensitive value without an obvious call-site change.
$options = @{
LiteralPath = './out'
Recurse = $false
File = $true
}
foreach ($name in 'Recurse', 'File') {
if ($settings.ContainsKey($name)) {
$options[$name] = [bool]$settings[$name]
}
}
Get-ChildItem @options
This example whitelists accepted keys and converts them to the target type. That is safer than using Update-Hashtable on a configuration object whose keys were not constrained. Explicit precedence is especially important for destination paths, recursive flags, deletion switches, and cloud account identifiers. A reviewer should be able to identify which source of configuration can override each value.
When a splat is passed across a function boundary, inspect what the callee receives in tests. Pester mocks can assert a hashtable of bound parameters without invoking the real filesystem or remote service. Include one test where the downstream command adds a new parameter, so a wrapper’s forwarding policy is deliberate rather than accidental. A stable wrapper should expose the options it supports and ignore or reject everything else clearly.
Copy only the parameters the wrapper intends to forward. Passing $PSBoundParameters directly is concise, but it couples the wrapper’s public parameter names to the downstream command’s names and may forward common parameters or control flags the callee was not designed to receive. A small allowlist preserves the abstraction:
$forward = @{}
foreach ($name in 'Name', 'Id') {
if ($PSBoundParameters.ContainsKey($name)) {
$forward[$name] = $PSBoundParameters[$name]
}
}
Get-Process @forward
If the wrapper renames a parameter, explicitly map the value to the downstream key. Keep -WhatIf, -Confirm, and -ErrorAction behavior intentional rather than assuming they automatically propagate across every nested call. Test the public wrapper from the caller’s perspective and verify that its own confirmation and error semantics remain intact after forwarding.
During a binding failure, log the names and runtime types of splat keys rather than dumping every value. This is often enough to find a misspelled key or a string where a Boolean was expected, while avoiding disclosure of secrets or identifiers. After correcting the options, add a regression assertion so later refactors do not silently change the callee’s received parameter set.
Related:
- PowerShell Advanced Function Parameters: Validation as an Input Contract
- PowerShell Module Manifests: Version, Requirements, and Public Surface
Sources: