Loosely Decoupled
ExplorationCode quality

PowerShell Splatting: Replace Long One-Liners with a Hashtable

Splatting forces every parameter of a long command to be named and laid out, which keeps it readable and safe to change years after it was written.

A patent-style drawing of a tired man with headphones round his neck squinting at a tangled strip of tape beside a tidy rack of pigeonholes holding the same information. The cord linking the rack is cyan.
On this page

Did you know a PowerShell command can take its parameters from a hashtable? The feature is called splatting. It has been in the language since PowerShell 2.0, nothing ever requires it, and so plenty of scripts are written without it.

Here is a line it exists to fix. It opens an incident on a status page through a REST API, with the token and the JSON body already in variables:

PowerShell
Invoke-RestMethod -Uri 'https://status.example.com/api/incidents' -Method Post -Headers @{Authorization="Bearer $token";Accept='application/json'} -ContentType 'application/json' -Body $body -TimeoutSec 30

It works. It’s also a line you have to read three times, and not because it’s wrong. It doesn’t scale — not in execution time, in comprehension time. The computer parses it in microseconds. A person parses it in minutes, even the person who wrote it.

Splatting turns that line into a block you can read at a glance, and it’s worth using less as a convenience than as a discipline.

Six months later

A script like this is usually written in one sitting, for a job that needs doing that evening, by someone who knows exactly what every argument is for. Commands run across several lines on backtick continuations, parameters stack one after another, a hashtable sits inline.

Six months later the person reading it doesn’t know, even when it’s the same person.

The failure isn’t correctness. The script still runs. The failure is that a one-line change, a different endpoint, means re-deriving what the whole line does before anyone dares touch it.

A script its own author can’t debug after six months has a problem, and so does every script written the same way.

What makes a line hard to read

Cognitive load is the effort it takes to hold something in your head long enough to understand it. In a command line it comes from a few places at once.

Syntax you have to parse while reading. Where does the list of extensions end? Is that parenthesis inside the script block or after it?

PowerShell
Get-ChildItem -Path D:\Exports -Include *.csv,*.json -Exclude *-keep.* -Recurse -File | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-14) } | Remove-Item -Force

Density. One line means one chunk. The brain can’t break it into pieces and check them separately.

Missing context. Why D:\Exports? Why fourteen days? What makes a file worth keeping? None of that is in the code; it’s in the head of whoever wrote it, or in a document somewhere else.

Any one of these is manageable. The one-liner stacks all three.

What splatting forces

Splatting takes two steps: put the command’s parameters in a hashtable, then hand the whole table to the command with @ in place of $:

PowerShell
$incident = @{
    Uri         = 'https://status.example.com/api/incidents'
    Method      = 'Post'
    Headers     = @{
        Authorization = "Bearer $token"
        Accept        = 'application/json'
    }
    ContentType = 'application/json'
    Body        = $body
    TimeoutSec  = 30
}

Invoke-RestMethod @incident

Same call. More lines. Far less to hold in your head, for four reasons.

Every parameter is named and on its own line. Nothing is positional; nothing depends on order.

The structure is visible. The headers are a nested block, not a run of braces in the middle of a line. You can see that the call posts JSON, with a bearer token, to one endpoint.

Changing it is safe. Adding a header is adding a line. Deleting a parameter is deleting a line. There’s no adjacent argument to break.

Comments fit.

PowerShell
$incident = @{
    Uri         = 'https://status.example.com/api/incidents'
    Method      = 'Post'
    Headers     = @{
        # Read the token from the vault, never from the script.
        Authorization = "Bearer $token"
        Accept        = 'application/json'
    }
    ContentType = 'application/json'
    Body        = $body
    # Fail fast; the caller retries.
    TimeoutSec  = 30
}

None of this is for the computer. It’s for the person reading the script later — probably you, and if not you, someone who can’t ask you.

People first, computers second

There’s an old Unix principle: write programs for people first, computers second. Not because the computer doesn’t matter, but because the human is the slow part. The machine executes the dense one-liner in the same time as the splatted block. The human pays for the density every single time they read it.

Splatting is a forcing function for that principle. To splat, you have to name every parameter, arrange them in a structure, and decide what you’re passing. You can’t throw arguments at a cmdlet and hope.

Rules that are not obvious

Splatting is simple until it isn’t. These are the parts that trip people up.

It’s @params at the call and $params everywhere else. The hashtable is a normal variable; only the call site uses @. Writing Invoke-RestMethod $incident passes the whole table as the first positional argument, and the error message will be about a type conversion, not about splatting.

Switch parameters take a boolean. Recurse = $true, not Recurse = 'yes' and not a bare Recurse. A hashtable value has to be a value.

You can mix splatted and explicit parameters. This is the pattern for a common base with a per-call difference:

PowerShell
$common = @{
    Headers    = @{ Authorization = "Bearer $token" }
    TimeoutSec = 30
}

Invoke-RestMethod @common -Uri "$api/components"
Invoke-RestMethod @common -Uri "$api/incidents"

PowerShell merges them. Naming the same parameter in both places is an error, which is what you want — a silent override would be worse.

You can build the table conditionally. A one-liner can’t express “add this parameter only if the variable is set”; a hashtable can:

PowerShell
$copy = @{
    Path        = $source
    Destination = $target
    Recurse     = $true
}
if ($DryRun) {
    $copy['WhatIf'] = $true
}

Copy-Item @copy

A function can pass its own parameters straight through. Inside a function, $PSBoundParameters is a hashtable of the parameters the caller supplied, and it splats:

PowerShell
function Get-RecentLog {
    param ([string]$Path, [switch]$Recurse)
    Get-ChildItem @PSBoundParameters -Filter *.log -File |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 10
}

Whatever the caller passed goes through unchanged, and whatever they didn’t pass isn’t mentioned at all. It’s the cleanest way to write a wrapper, and it only exists because splatting does.

Arrays splat positionally. @('a', 'b') splatted fills parameters by position, not by name. It’s legal and almost never what you want in a script you intend to read later.

Bigger commands, bigger differences

The gap between the two forms grows with the command. Here is the incident call once it runs from a server. It goes through a proxy, sends a user agent the status page can identify, and retries when the status page answers with an error.

PowerShell
Invoke-RestMethod -Uri 'https://status.example.com/api/incidents' -Method Post -Headers @{Authorization="Bearer $token";Accept='application/json';'X-Request-Id'=$requestId} -ContentType 'application/json' -Body $body -TimeoutSec 30 -UserAgent 'deploy-bot/2.1' -Proxy 'http://proxy.corp.example:8080' -ProxyUseDefaultCredentials -MaximumRetryCount 3 -RetryIntervalSec 5

One command, eleven parameters, one of them a table. Now the same call, splatted:

PowerShell
$incident = @{
    Uri                        = 'https://status.example.com/api/incidents'
    Method                     = 'Post'
    Headers                    = @{
        Authorization  = "Bearer $token"
        Accept         = 'application/json'
        'X-Request-Id' = $requestId
    }
    ContentType                = 'application/json'
    Body                       = $body
    TimeoutSec                 = 30
    UserAgent                  = 'deploy-bot/2.1'
    Proxy                      = 'http://proxy.corp.example:8080'
    ProxyUseDefaultCredentials = $true
    MaximumRetryCount          = 3
    RetryIntervalSec           = 5
}

Invoke-RestMethod @incident

Longer, and easier to understand, modify and debug. When the proxy changes, the proxy is one line you can find. MaximumRetryCount and RetryIntervalSec need PowerShell 7. To run the same call on Windows PowerShell 5.1, you delete those two lines; nothing around them changes.

When not to splat

Splatting is a tool for commands that are hard to read. Applying it to commands that aren’t adds lines and nothing else.

One or two parameters. Get-Content 'settings.json' doesn’t improve with a hashtable.

Interactive use. At the prompt you’re the only reader, and you’re reading it right now.

Idioms everyone knows. Get-ChildItem | Where-Object Name -like '*.log' is a pattern PowerShell users recognize on sight; splatting it hides the idiom.

The rule I use: splat when future-you would have to re-read the line to understand it. For most scripts that turns out to be most of the non-trivial calls, and almost none of the trivial ones.

Documentation you no longer need

A splatted call documents itself in two places. The variable name says what the call is for; the table says how:

PowerShell
$openIncident = @{
    Uri         = 'https://status.example.com/api/incidents'
    Method      = 'Post'
    Headers     = @{
        Authorization = "Bearer $token"
        Accept        = 'application/json'
    }
    ContentType = 'application/json'
    Body        = $body
    TimeoutSec  = 30
}

Invoke-RestMethod @openIncident

The one-liner needed a comment above it to say what it did. This doesn’t. Comments still earn their place for why — why this endpoint, why this timeout — but what is now in the code, where it can’t drift from the code.

Readability compounds

Every future change to a script starts with reading it. A bug fix, a new parameter, an upgrade to a newer module: read first, then edit. The cost of an unreadable line is paid on every one of those visits, for as long as the script is in use. The scripts that matter stay in use for years.

Splatting is a small feature. What it represents is larger: optimize for the person who will maintain the code, not the machine that will run it. The machine is fast and doesn’t care whether the call is one line or twenty. The person is slow and cares whether they can understand it without an afternoon of archaeology.

Splatting is how a script written in one evening stays usable for the next several years.

References

New articles, by email.

A short note when something new is published.