<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Code quality · Loosely Decoupled</title>
    <link>https://looselydecoupled.com/topics/code-quality/</link>
    <description>Readability, cognitive load, and the practices that hold up as a team grows.</description>
    <language>en-us</language>
    
    <copyright>© 2026 Emmanuel Tsouris</copyright>
    <lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://looselydecoupled.com/topics/code-quality/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Why Moving $null to the Left Side Broke My PowerShell Script</title>
      <link>https://looselydecoupled.com/powershell-null-left-side-comparison-array/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/powershell-null-left-side-comparison-array/</guid>
      <pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>PowerShell</category>
      <category>Code quality</category>
      
      <description>The analyzer&#39;s fix is safe only where code meant a null check; this function relied on the array filter in ways only empty and nearly empty inputs reveal.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>PSScriptAnalyzer&rsquo;s <code>PSPossibleIncorrectComparisonWithNull</code> rule is right for
most code. When the left side of <code>-ne</code> is an array, PowerShell filters the array instead
of returning true or false. These two tests ask different questions:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Does $items hold at least one item that is not null?</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$items</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;has items&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Is the variable itself something other than null?</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$items</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;not null&#39;</span> <span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>An empty array fails the first test and passes the second. If the code you are
cleaning up relies on the first, state that meaning in a form the analyzer
accepts. It differs in one case: when the filter leaves exactly one item and
that item is falsy, such as <code>0</code>, <code>''</code>, <code>$false</code> or an empty array. That case
is covered below:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-HasItem</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span><span class="nv">$Value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="vm">@</span><span class="p">(</span><span class="nv">$Value</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$_</span> <span class="p">}).</span><span class="py">Count</span> <span class="o">-gt</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$items</span><span class="p">)</span> <span class="p">{</span> <span class="s1">&#39;has items&#39;</span> <span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div>
</div>

<p>I ran into this cleaning up old PowerShell that someone else wrote. The plan
was mechanical: run PSScriptAnalyzer and take its suggestions one at a time.
One of them came from the rule <code>PSPossibleIncorrectComparisonWithNull</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$null should be on the left side of equality comparisons.</span></span></code></pre></div></div>
  </div>
</div><p>I flipped the comparisons, and the warning went away. So did part of what the
function did. The original author had written <code>$array -ne $null</code> on purpose,
and the function depended on the behavior that the rule exists to warn about.</p>
<p>That&rsquo;s why I wanted to write it up. Nothing here is a PowerShell bug, and the
rule gives good advice for most code. This code was the exception: the author
meant what they wrote, and fixing it broke it.</p>
<!-- style-check: procedure -->
<h2 id="reproduce-the-difference">Reproduce the difference<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#reproduce-the-difference"
     aria-label="Link to section: Reproduce the difference" data-pagefind-ignore></a>
</h2>
<p>In Windows PowerShell 5.1 or PowerShell 7, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">$values</span> <span class="console__token">=</span> <span class="console__token">@()</span>
</span><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">if</span> <span class="console__token">($values</span> <span class="console__token">-ne</span> <span class="console__token">$null)</span> <span class="console__token">{</span> <span class="console__token">&#39;has</span> <span class="console__token">values&#39;</span> <span class="console__token">}</span> <span class="console__token">else</span> <span class="console__token">{</span> <span class="console__token">&#39;nothing</span> <span class="console__token">to</span> <span class="console__token">add&#39;</span> <span class="console__token">}</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">nothing</span> <span class="console__token">to</span> <span class="console__token">add</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">if</span> <span class="console__token">($null</span> <span class="console__token">-ne</span> <span class="console__token">$values)</span> <span class="console__token">{</span> <span class="console__token">&#39;has</span> <span class="console__token">values&#39;</span> <span class="console__token">}</span> <span class="console__token">else</span> <span class="console__token">{</span> <span class="console__token">&#39;nothing</span> <span class="console__token">to</span> <span class="console__token">add&#39;</span> <span class="console__token">}</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">has</span> <span class="console__token">values</span></span></code></pre>
    </div>
  </div></div><p>The same variable, compared with the same operator, gives opposite answers.</p>
<!-- style-check: end procedure -->
<h2 id="why-the-two-comparisons-disagree">Why the two comparisons disagree<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#why-the-two-comparisons-disagree"
     aria-label="Link to section: Why the two comparisons disagree" data-pagefind-ignore></a>
</h2>
<p>An equality or matching operator with a collection on its left filters the
collection. It
returns every item for which the comparison is true, as an array.
<a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators" rel="external noopener" target="_blank">about_Comparison_Operators<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
documents this for every equality and matching operator.</p>
<p>So <code>@() -ne $null</code> returns an empty array, and <code>if</code> converts an empty array to
<code>$false</code>. <code>$null -ne @()</code> has a scalar on the left, so it compares once and
returns <code>$true</code>, because an empty array isn&rsquo;t <code>$null</code>.</p>
<p>That&rsquo;s why the analyzer asks for <code>$null</code> on the left: most code that writes
<code>$x -ne $null</code> means &ldquo;is <code>$x</code> null?&rdquo;, and gets a filter instead when <code>$x</code>
happens to be an array. The rule&rsquo;s own
<a href="https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/possibleincorrectcomparisonwithnull" rel="external noopener" target="_blank">documentation<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
says the same. The rule is correct about that code. It can&rsquo;t tell that
somebody&rsquo;s code wanted the filter.</p>
<p>The filter has a second property, which matters later. <code>if</code> treats an array
that holds exactly one item as that item. <code>@(0) -ne $null</code> returns <code>@(0)</code>, and
<code>if (@(0))</code> is <code>$false</code> because <code>0</code> is.</p>
<h2 id="what-the-original-code-meant">What the original code meant<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#what-the-original-code-meant"
     aria-label="Link to section: What the original code meant" data-pagefind-ignore></a>
</h2>
<p>The function built combinations. Each call took the rows built so far and a set
of values, and appended every value to every row. The two comparisons that the
analyzer flagged are highlighted:</p>
<div class="code-block" data-copyable data-copy-label="Add-ToAll.ps1">
  <div class="code-block__head"><span class="code-block__lang code-block__file">Add-ToAll.ps1</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Add-ToAll</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span> <span class="nv">$currentResult</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span> <span class="nv">$valuesToAdd</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nv">$returnValue</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span>
</span></span><span class="line hl"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$valuesToAdd</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$value</span> <span class="k">in</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line hl"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nv">$currentResult</span> <span class="o">-ne</span> <span class="vm">$null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">foreach</span> <span class="p">(</span><span class="nv">$result</span> <span class="k">in</span> <span class="nv">$currentResult</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$newResult</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="nv">$result</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$newResult</span> <span class="p">+=</span> <span class="p">,</span><span class="nv">$value</span>
</span></span><span class="line"><span class="cl">                    <span class="nv">$returnValue</span> <span class="p">+=</span> <span class="p">,</span><span class="nv">$newResult</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nv">$returnValue</span> <span class="p">+=</span> <span class="p">,</span><span class="vm">@</span><span class="p">(,</span><span class="nv">$value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$returnValue</span> <span class="p">=</span> <span class="nv">$currentResult</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">,</span><span class="nv">$returnValue</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>Both comparisons use the filter on purpose:</p>
<ul>
<li><code>$valuesToAdd -ne $null</code> is false for an empty array, so there&rsquo;s nothing to
add and the rows come back unchanged.</li>
<li><code>$currentResult -ne $null</code> is false for an empty array, so there are no rows
yet and each value starts one.</li>
</ul>
<p>The first call passes <code>@()</code> as <code>$currentResult</code> to start the chain. With the
operands flipped, <code>$null -ne @()</code> is true, the inner loop runs over nothing,
and the first call returns an empty array. Every later call appends to no rows
and also returns nothing.</p>
<p>Each version, run on the same inputs, with its result as JSON:</p>
<div class="table-wrap" data-scrollable data-label="Table: Rows so far">
  <table>
    <thead>
      <tr><th scope="col">Rows so far</th><th scope="col">Values to add</th><th scope="col">Original returns</th><th scope="col">Flipped returns</th></tr>
    </thead>
    <tbody>
      <tr><td><code>@()</code></td><td><code>@()</code></td><td><code>[]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@()</code></td><td><code>@('A', 'B')</code></td><td><code>[[&quot;A&quot;],[&quot;B&quot;]]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@()</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@($null)</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[[&quot;X&quot;,null],[&quot;Y&quot;,null]]</code></td></tr>
      <tr><td><code>@(,@('X'))</code></td><td><code>'A'</code></td><td><code>[[&quot;X&quot;,&quot;A&quot;]]</code></td><td><code>[[&quot;X&quot;,&quot;A&quot;]]</code></td></tr>
      <tr><td><code>@(@('X'), @('Y'))</code></td><td><code>@(0)</code></td><td><code>[[&quot;X&quot;],[&quot;Y&quot;]]</code></td><td><code>[[&quot;X&quot;,0],[&quot;Y&quot;,0]]</code></td></tr>
    </tbody>
  </table>
</div>
<p>Two rows still agree: both inputs empty, and the ordinary case, a real value
added to rows that exist. A test suite built from typical inputs passes, and the change surfaces in
production, on the first empty input.</p>
<!-- style-check: procedure -->
<h2 id="pin-the-current-behavior-in-a-pester-test">Pin the current behavior in a Pester test<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#pin-the-current-behavior-in-a-pester-test"
     aria-label="Link to section: Pin the current behavior in a Pester test" data-pagefind-ignore></a>
</h2>
<p>The analyzer&rsquo;s fix looked safe because nothing checked what the function
returned. Before you change inherited code, write a characterization test: run
the current code on chosen inputs and assert what it returns now, whether or
not that looks right. A refactor then has to keep every answer or change one
on purpose.</p>
<p>This test for Pester 5, the PowerShell test framework, turns the earlier table
into cases. Save the function as <code>Add-ToAll.ps1</code> in the same folder:</p>
<div class="code-block" data-copyable data-copy-label="Add-ToAll.Tests.ps1">
  <div class="code-block__head"><span class="code-block__lang code-block__file">Add-ToAll.Tests.ps1</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">BeforeAll</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span> <span class="nv">$PSScriptRoot</span><span class="p">/</span><span class="nb">Add-ToAll</span><span class="p">.</span><span class="py">ps1</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Describe</span> <span class="s1">&#39;Add-ToAll&#39;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">It</span> <span class="s1">&#39;&lt;Name&gt;&#39;</span> <span class="n">-TestCases</span> <span class="vm">@</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;both empty&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;start from empty&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;A&#34;],[&#34;B&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">();</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;A&#39;</span><span class="p">,</span> <span class="s1">&#39;B&#39;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;nothing to add&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">()</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;add null&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">$null</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;one value&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;,&#34;A&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(,</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="s1">&#39;A&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="vm">@</span><span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s1">&#39;a single zero&#39;</span><span class="p">;</span> <span class="n">Json</span> <span class="p">=</span> <span class="s1">&#39;[[&#34;X&#34;],[&#34;Y&#34;]]&#39;</span>
</span></span><span class="line"><span class="cl">           <span class="n">Rows</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="vm">@</span><span class="p">(</span><span class="s1">&#39;X&#39;</span><span class="p">),</span> <span class="vm">@</span><span class="p">(</span><span class="s1">&#39;Y&#39;</span><span class="p">));</span> <span class="n">Values</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nv">$result</span> <span class="p">=</span> <span class="nb">Add-ToAll</span> <span class="n">-currentResult</span> <span class="nv">$Rows</span> <span class="n">-valuesToAdd</span> <span class="nv">$Values</span>
</span></span><span class="line"><span class="cl">        <span class="nb">ConvertTo-Json</span> <span class="n">-InputObject</span> <span class="nv">$result</span> <span class="n">-Compress</span> <span class="p">|</span> <span class="n">Should</span> <span class="n">-Be</span> <span class="nv">$Json</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>The cases that matter are the ones the old comparison treated specially: an
empty array, an array of nulls and a single falsy value. They are where a
flipped comparison changes the result, and they are the inputs a typical test
suite leaves out.</p>
<p>Run <code>Invoke-Pester ./Add-ToAll.Tests.ps1</code> before you touch the function. Every
case passes, because each expected value was copied from the original&rsquo;s
output. Flip the two comparisons and four cases fail: start from empty,
nothing to add, add null and a single zero.</p>
<div class="admonition admonition--note" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-info"/></svg>
    <span>Note</span>
  </p>
  <p>Windows PowerShell 5.1 ships with Pester 3.4, which runs this file
differently. Install Pester 5 first; the
<a href="https://pester.dev/docs/introduction/installation" rel="external noopener" target="_blank">Pester installation guide<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
covers replacing the built-in version.</p>
</div><p>Each case compares JSON text instead of the array itself. Pass the result to
<code>ConvertTo-Json</code> with <code>-InputObject</code>, not through the pipeline:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">@(,@(&#39;X&#39;,&#39;A&#39;))</span> <span class="console__token">|</span> <span class="console__token">ConvertTo-Json</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">@(&#39;X&#39;,&#39;A&#39;)</span> <span class="console__token">|</span> <span class="console__token">ConvertTo-Json</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">ConvertTo-Json</span> <span class="console__token">-InputObject</span> <span class="console__token">@(,@(&#39;X&#39;,&#39;A&#39;))</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[[&#34;X&#34;,&#34;A&#34;]]</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">ConvertTo-Json</span> <span class="console__token">-InputObject</span> <span class="console__token">@(&#39;X&#39;,&#39;A&#39;)</span> <span class="console__token">-Compress</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[&#34;X&#34;,&#34;A&#34;]</span></span></code></pre>
    </div>
  </div></div><p>That output is PowerShell 7. Windows PowerShell 5.1 prints the first line as
<code>{&quot;value&quot;:[&quot;X&quot;,&quot;A&quot;],&quot;Count&quot;:2}</code>, which is one more reason to use
<code>-InputObject</code>.</p>
<p>The pipeline sends the items of an array, not the array, and that loses
information in two ways:</p>
<ul>
<li>A result with one row loses a level of nesting. One row, <code>[[&quot;X&quot;,&quot;A&quot;]]</code>, and
a flat row, <code>[&quot;X&quot;,&quot;A&quot;]</code>, serialize to the same text, so a refactor that
returned the wrong shape would pass.</li>
<li>An empty result sends nothing, so <code>ConvertTo-Json</code> writes nothing. With
<code>-InputObject</code>, the same result is written as <code>[]</code>.</li>
</ul>
<p>When you write the cases, two inputs look different and are not:</p>
<ul>
<li><code>@(@())</code> is an empty array, not an array that holds one empty array. The
array subexpression operator does not nest. Write <code>,@()</code> for the nested one.</li>
<li><code>@(@('X'))</code> is <code>@('X')</code>, for the same reason. Write <code>@(,@('X'))</code>.</li>
</ul>
<h2 id="make-the-intent-explicit">Make the intent explicit<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#make-the-intent-explicit"
     aria-label="Link to section: Make the intent explicit" data-pagefind-ignore></a>
</h2>
<p>Replace each filtering comparison with a test that says what it checks. Keep
the function&rsquo;s structure and change only the two conditions:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># True when $Value holds at least one item that is not null. An empty array</span>
</span></span><span class="line"><span class="cl"><span class="c"># and an array of nulls hold none, which is what `$Value -ne $null` tested.</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Test-HasItem</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span><span class="p">(</span><span class="nv">$Value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="vm">@</span><span class="p">(</span><span class="nv">$Value</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="vm">$null</span> <span class="o">-ne</span> <span class="nv">$_</span> <span class="p">}).</span><span class="py">Count</span> <span class="o">-gt</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="nv">$value</span> <span class="k">in</span> <span class="nv">$valuesToAdd</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nb">Test-HasItem</span> <span class="nv">$currentResult</span><span class="p">)</span> <span class="p">{</span></span></span></code></pre></div></div>
  </div>
</div><p>PSScriptAnalyzer reports nothing for this version. Run the test again and
one case fails: a single zero. Given <code>@(0)</code>, the explicit version returns
<code>[[&quot;X&quot;,0],[&quot;Y&quot;,0]]</code>, and the original returned the rows unchanged.</p>
<p>The original treated <code>@(0)</code> as nothing to add, because an array with one item
converts to that item and <code>0</code> is false. The same is true of <code>@('')</code>,
<code>@($false)</code> and <code>@(,@())</code>, an array holding one empty array, and of any input
the filter reduces to one such item, such as <code>@($null, 0)</code>. That is the one-item rule from earlier, not a decision anyone
made, and the explicit version drops it. If your data can contain a single
zero, empty string, <code>$false</code> or empty array, decide which behavior is correct. Change that
case&rsquo;s expected value only if the new behavior is the one you want.</p>
<!-- style-check: end procedure -->
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/powershell-null-left-side-comparison-array/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<p><strong>The warning is on <code>-eq</code>, not <code>-ne</code>.</strong> The same filter applies.
<code>@($null, $null) -eq $null</code> returns two nulls, and an array with two items is
true in an <code>if</code>, even though both items are null.</p>
<p><strong>The left side is never an array.</strong> Then flipping the comparison changes
nothing, and it guards against the day it becomes one. Check where the value
comes from: a cmdlet that returns one object returns it bare, and one that
returns several returns an array.</p>
<p><strong>You want to keep the old line and silence the rule.</strong> Suppress it on that
function with a reason, so the next person reads why:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="n">Diagnostics</span><span class="p">.</span><span class="py">CodeAnalysis</span><span class="p">.</span><span class="py">SuppressMessageAttribute</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;PSPossibleIncorrectComparisonWithNull&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Justification</span> <span class="p">=</span> <span class="s1">&#39;Empty array means nothing to add.&#39;</span><span class="p">)]</span></span></span></code></pre></div></div>
  </div>
</div><p>The attribute goes above the function&rsquo;s <code>param()</code> block. The explicit test is
still the better fix, because the reason then lives in the code rather than in
a comment about it.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>Windows PowerShell</dt> <dd>5.1 on Windows</dd></div>
    <div class="tested__row"><dt>PowerShell</dt> <dd>7 on macOS, 7.4.20 on Ubuntu 24.04</dd></div>
    <div class="tested__row"><dt>PSScriptAnalyzer</dt> <dd>1.23.0, 1.25.0</dd></div>
    <div class="tested__row"><dt>Pester</dt> <dd>5.9.1</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>September 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>PowerShell Parameter Validation for Safer Scripts</title>
      <link>https://looselydecoupled.com/powershell-parameter-validation/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/powershell-parameter-validation/</guid>
      <pubDate>Wed, 15 Jan 2025 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>PowerShell</category>
      <category>Code quality</category>
      <category>Automation</category>
      
      <description>Validation attributes stop a PowerShell script before it runs on bad input, but ValidateSet and ValidatePattern ignore case unless told not to.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>Add a validation attribute to each parameter: <code>ValidateSet</code> for a fixed list,
<code>ValidatePattern</code> for a format, <code>ValidateRange</code> for a number and
<code>ValidateScript</code> for anything else. PowerShell checks every value a caller
passes before the script&rsquo;s first line runs, and stops with an error naming the
parameter. It never checks a default, so keep each default inside its own rule:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c">#</span><span class="k">Requires</span><span class="w"> </span><span class="kd">-Version</span><span class="na"> 7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">param</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">ValidateSet</span><span class="p">(</span><span class="s1">&#39;dev&#39;</span><span class="p">,</span> <span class="s1">&#39;staging&#39;</span><span class="p">,</span> <span class="s1">&#39;prod&#39;</span><span class="p">,</span> <span class="n">IgnoreCase</span> <span class="p">=</span> <span class="vm">$false</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$Environment</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">ValidatePattern</span><span class="p">(</span><span class="s1">&#39;^app-[a-z0-9-]+\z&#39;</span><span class="p">,</span> <span class="n">Options</span> <span class="p">=</span> <span class="s1">&#39;None&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">ErrorMessage</span> <span class="p">=</span> <span class="s1">&#39;{0} is not a service name. Use app- and lowercase letters, digits or hyphens.&#39;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$ServiceName</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">ValidateRange</span><span class="p">(</span><span class="mf">1</span><span class="p">,</span> <span class="mf">365</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$DaysToKeep</span> <span class="p">=</span> <span class="mf">30</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="nb">ValidateScript</span><span class="p">({</span> <span class="nb">Test-Path</span> <span class="n">-Path</span> <span class="nv">$_</span> <span class="n">-PathType</span> <span class="n">Container</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">ErrorMessage</span> <span class="p">=</span> <span class="s1">&#39;Folder {0} does not exist.&#39;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$LogRoot</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
  </div>
</div><p><code>ValidateSet</code> and <code>ValidatePattern</code> ignore case by default, so <code>Prod</code> passes a
set of <code>prod</code>. <code>IgnoreCase = $false</code> and <code>Options = 'None'</code> make them exact.
Windows PowerShell 5.1 can&rsquo;t parse this <code>param</code> block, because <code>ErrorMessage</code>
exists from PowerShell 6 on; drop that argument to run it there.</p>

</div>

<p>A script that deletes, deploys or overwrites is only as safe as the values it&rsquo;s
given. A <code>-DaysToKeep</code> of 0 on a log cleanup removes every log; an environment
typed <code>Prod</code> instead of <code>prod</code> can slip past a later <code>if ($Environment -ceq 'prod')</code> and take the wrong branch. Validation attributes move those checks to
the parameter itself, where PowerShell enforces them on every value passed in,
before anything runs, and reports which parameter was wrong.</p>
<p>The example is a log cleanup script, <code>Remove-OldLog.ps1</code>, with four parameters.
Each section adds the rule for one of them.</p>
<!-- style-check: procedure -->
<h2 id="make-the-required-parameters-mandatory">Make the required parameters mandatory<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#make-the-required-parameters-mandatory"
     aria-label="Link to section: Make the required parameters mandatory" data-pagefind-ignore></a>
</h2>
<p><code>[Parameter(Mandatory)]</code> makes PowerShell refuse to run without a value. Run
interactively, it prompts for the missing value; run with <code>-NonInteractive</code>,
it stops with an error instead. It also
rejects an empty string, so <code>-ServiceName ''</code> fails before any other rule sees
it.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="nb">Parameter</span><span class="p">(</span><span class="na">Mandatory</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$ServiceName</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="limit-a-value-to-a-list-with-validateset">Limit a value to a list with <code>ValidateSet</code><a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#limit-a-value-to-a-list-with-validateset"
     aria-label="Link to section: Limit a value to a list with ValidateSet" data-pagefind-ignore></a>
</h2>
<p><code>ValidateSet</code> accepts only the values it names, and tab completion offers
them. It compares without case unless <code>IgnoreCase</code> is <code>$false</code>; with the
default, <code>Prod</code> passes and reaches the script with its capital P.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="nb">ValidateSet</span><span class="p">(</span><span class="s1">&#39;dev&#39;</span><span class="p">,</span> <span class="s1">&#39;staging&#39;</span><span class="p">,</span> <span class="s1">&#39;prod&#39;</span><span class="p">,</span> <span class="n">IgnoreCase</span> <span class="p">=</span> <span class="vm">$false</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$Environment</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="check-a-format-with-validatepattern">Check a format with <code>ValidatePattern</code><a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#check-a-format-with-validatepattern"
     aria-label="Link to section: Check a format with ValidatePattern" data-pagefind-ignore></a>
</h2>
<p><code>ValidatePattern</code> matches the value against a regular expression. Like
<code>ValidateSet</code>, it ignores case by default; <code>Options = 'None'</code> turns that off.
Its default error message repeats the regular expression back at the reader,
so give it an <code>ErrorMessage</code> that says what a valid value looks like. <code>{0}</code> is
the value that failed.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="nb">ValidatePattern</span><span class="p">(</span><span class="s1">&#39;^app-[a-z0-9-]+\z&#39;</span><span class="p">,</span> <span class="n">Options</span> <span class="p">=</span> <span class="s1">&#39;None&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">ErrorMessage</span> <span class="p">=</span> <span class="s1">&#39;{0} is not a service name. Use app- and lowercase letters, digits or hyphens.&#39;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$ServiceName</span></span></span></code></pre></div></div>
  </div>
</div><p>Anchor the pattern with <code>^</code> and <code>\z</code>. Without anchors, <code>app-api; Remove-Item</code>
passes because it contains a match. Use <code>\z</code> rather than <code>$</code>: in .NET, <code>$</code> also
matches before a trailing newline, so <code>app-api</code> followed by a newline passes
<code>^app-[a-z0-9-]+$</code> and reaches the script with the newline still in it.</p>
<h2 id="bound-a-number-with-validaterange">Bound a number with <code>ValidateRange</code><a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#bound-a-number-with-validaterange"
     aria-label="Link to section: Bound a number with ValidateRange" data-pagefind-ignore></a>
</h2>
<p><code>ValidateRange</code> sets an inclusive minimum and maximum. For a cleanup script,
the minimum is the safety rule: a 0 here would delete every log up to now.</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="nb">ValidateRange</span><span class="p">(</span><span class="mf">1</span><span class="p">,</span> <span class="mf">365</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">int</span><span class="p">]</span><span class="nv">$DaysToKeep</span> <span class="p">=</span> <span class="mf">30</span></span></span></code></pre></div></div>
  </div>
</div><h2 id="check-anything-else-with-validatescript">Check anything else with <code>ValidateScript</code><a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#check-anything-else-with-validatescript"
     aria-label="Link to section: Check anything else with ValidateScript" data-pagefind-ignore></a>
</h2>
<p><code>ValidateScript</code> runs a script block with the value in <code>$_</code> and accepts it
when the block&rsquo;s result is truthy. A result of 0, an empty string, <code>$null</code> or
no output fails it as surely as <code>$false</code>, so end the block on a test that
returns <code>$true</code>. Use it for checks no other attribute makes, such as
whether a folder exists.</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">[</span><span class="nb">ValidateScript</span><span class="p">({</span> <span class="nb">Test-Path</span> <span class="n">-Path</span> <span class="nv">$_</span> <span class="n">-PathType</span> <span class="n">Container</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="n">ErrorMessage</span> <span class="p">=</span> <span class="s1">&#39;Folder {0} does not exist.&#39;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="no">string</span><span class="p">]</span><span class="nv">$LogRoot</span></span></span></code></pre></div></div>
  </div>
</div><!-- style-check: end procedure -->
<h2 id="expected-result">Expected result<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#expected-result"
     aria-label="Link to section: Expected result" data-pagefind-ignore></a>
</h2>
<p>Valid input runs the script. Anything else stops it before the first line, with
the parameter named:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">PS&gt; </span><span class="console__token">./Remove-OldLog.ps1</span> <span class="console__token">-Environment</span> <span class="console__token">prod</span> <span class="console__token">-ServiceName</span> <span class="console__token">app-api</span> <span class="console__token">-DaysToKeep</span> <span class="console__token">0</span> <span class="console__token">-LogRoot</span> <span class="console__token">./logs</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Remove-OldLog.ps1:</span> <span class="console__token">Cannot</span> <span class="console__token">validate</span> <span class="console__token">argument</span> <span class="console__token">on</span> <span class="console__token">parameter</span> <span class="console__token">&#39;DaysToKeep&#39;.</span> <span class="console__token">The</span> <span class="console__token">0</span> <span class="console__token">argument</span> <span class="console__token">is</span> <span class="console__token">less</span> <span class="console__token">than</span> <span class="console__token">the</span> <span class="console__token">minimum</span> <span class="console__token">allowed</span> <span class="console__token">range</span> <span class="console__token">of</span> <span class="console__token">1.</span> <span class="console__token">Supply</span> <span class="console__token">an</span> <span class="console__token">argument</span> <span class="console__token">that</span> <span class="console__token">is</span> <span class="console__token">greater</span> <span class="console__token">than</span> <span class="console__token">or</span> <span class="console__token">equal</span> <span class="console__token">to</span> <span class="console__token">1</span> <span class="console__token">and</span> <span class="console__token">then</span> <span class="console__token">try</span> <span class="console__token">the</span> <span class="console__token">command</span> <span class="console__token">again.</span></span></code></pre>
    </div>
  </div></div><p>The error is terminating, so a caller&rsquo;s <code>try</code>/<code>catch</code> sees it, and nothing in
the script body has run.</p>
<h2 id="common-failures">Common failures<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#common-failures"
     aria-label="Link to section: Common failures" data-pagefind-ignore></a>
</h2>
<h3 id="validateset-and-validatepattern-ignore-case-by-default"><code>ValidateSet</code> and <code>ValidatePattern</code> ignore case by default<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#validateset-and-validatepattern-ignore-case-by-default"
     aria-label="Link to section: ValidateSet and ValidatePattern ignore case by default" data-pagefind-ignore></a>
</h3>
<p><code>ValidateSet</code> and <code>ValidatePattern</code> compare without case unless told
otherwise, and the value reaches the script exactly as typed. Add
<code>IgnoreCase = $false</code> to <code>ValidateSet</code> and <code>Options = 'None'</code> to
<code>ValidatePattern</code>. With them, the mistyped value stops at the boundary:</p>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Remove-OldLog.ps1:</span> <span class="console__token">Cannot</span> <span class="console__token">validate</span> <span class="console__token">argument</span> <span class="console__token">on</span> <span class="console__token">parameter</span> <span class="console__token">&#39;Environment&#39;.</span> <span class="console__token">The</span> <span class="console__token">argument</span> <span class="console__token">&#34;Prod&#34;</span> <span class="console__token">does</span> <span class="console__token">not</span> <span class="console__token">belong</span> <span class="console__token">to</span> <span class="console__token">the</span> <span class="console__token">set</span> <span class="console__token">&#34;dev,staging,prod&#34;</span> <span class="console__token">specified</span> <span class="console__token">by</span> <span class="console__token">the</span> <span class="console__token">ValidateSet</span> <span class="console__token">attribute.</span> <span class="console__token">Supply</span> <span class="console__token">an</span> <span class="console__token">argument</span> <span class="console__token">that</span> <span class="console__token">is</span> <span class="console__token">in</span> <span class="console__token">the</span> <span class="console__token">set</span> <span class="console__token">and</span> <span class="console__token">then</span> <span class="console__token">try</span> <span class="console__token">the</span> <span class="console__token">command</span> <span class="console__token">again.</span></span></code></pre>
    </div>
  </div></div><h3 id="default-values-are-never-validated">Default values are never validated<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#default-values-are-never-validated"
     aria-label="Link to section: Default values are never validated" data-pagefind-ignore></a>
</h3>
<p>PowerShell validates the values a caller passes, never the default. A
<code>[ValidateRange(1, 365)]</code> parameter with a default of 0 runs with 0. Check each
default against its own rule when you write it.</p>
<h3 id="invalid-assignment-inside-the-script-doesnt-stop-it">Invalid assignment inside the script doesn&rsquo;t stop it<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#invalid-assignment-inside-the-script-doesnt-stop-it"
     aria-label="Link to section: Invalid assignment inside the script doesn’t stop it" data-pagefind-ignore></a>
</h3>
<p>The attribute stays on the variable, so a later assignment is checked too. A
failed check on an assignment ends only that statement: PowerShell prints the
error, keeps the old value and runs the next line.</p>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">MetadataError:</span> <span class="console__token">&lt;folder&gt;/Remove-OldLog.ps1:7</span>
</span><span class="line"><span class="console__token">Line</span> <span class="console__token">|</span>
</span><span class="line">   <span class="console__token">7</span> <span class="console__token">|</span>  <span class="console__token">$DaysToKeep</span> <span class="console__token">=</span> <span class="console__token">0</span>
</span><span class="line">     <span class="console__token">|</span>  <span class="console__token">~~~~~~~~~~~~~~~</span>
</span><span class="line">     <span class="console__token">|</span> <span class="console__token">The</span> <span class="console__token">variable</span> <span class="console__token">cannot</span> <span class="console__token">be</span> <span class="console__token">validated</span> <span class="console__token">because</span> <span class="console__token">the</span> <span class="console__token">value</span> <span class="console__token">0</span> <span class="console__token">is</span> <span class="console__token">not</span> <span class="console__token">a</span> <span class="console__token">valid</span>
</span><span class="line">     <span class="console__token">|</span> <span class="console__token">value</span> <span class="console__token">for</span> <span class="console__token">the</span> <span class="console__token">DaysToKeep</span> <span class="console__token">variable.</span></span></code></pre>
    </div>
  </div></div><p>Treat that message as a bug in the script. <code>try</code>/<code>catch</code> catches it, and
<code>$ErrorActionPreference = 'Stop'</code> at the top makes it end the run.</p>
<h3 id="validatepatterns-default-error-shows-the-regular-expression"><code>ValidatePattern</code>&rsquo;s default error shows the regular expression<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#validatepatterns-default-error-shows-the-regular-expression"
     aria-label="Link to section: ValidatePattern’s default error shows the regular expression" data-pagefind-ignore></a>
</h3>
<p>Without an <code>ErrorMessage</code>, a failed pattern reports the pattern itself:</p>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Remove-OldLog.ps1:</span> <span class="console__token">Cannot</span> <span class="console__token">validate</span> <span class="console__token">argument</span> <span class="console__token">on</span> <span class="console__token">parameter</span> <span class="console__token">&#39;ServiceName&#39;.</span> <span class="console__token">The</span> <span class="console__token">argument</span> <span class="console__token">&#34;API&#34;</span> <span class="console__token">does</span> <span class="console__token">not</span> <span class="console__token">match</span> <span class="console__token">the</span> <span class="console__token">&#34;^app-[a-z0-9-]+\z&#34;</span> <span class="console__token">pattern.</span> <span class="console__token">Supply</span> <span class="console__token">an</span> <span class="console__token">argument</span> <span class="console__token">that</span> <span class="console__token">matches</span> <span class="console__token">&#34;^app-[a-z0-9-]+\z&#34;</span> <span class="console__token">and</span> <span class="console__token">try</span> <span class="console__token">the</span> <span class="console__token">command</span> <span class="console__token">again.</span></span></code></pre>
    </div>
  </div></div><p>Add <code>ErrorMessage</code> to <code>ValidatePattern</code> and <code>ValidateScript</code>, as in the earlier
steps. It exists from PowerShell 6 on, so Windows PowerShell 5.1 can&rsquo;t run a
<code>param</code> block that uses it; the <code>#Requires -Version 7</code> line at the top of the
script states that requirement before anything else. I tested only PowerShell
7.6.6. <a href="https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters" rel="external noopener" target="_blank">about_Functions_Advanced_Parameters on Microsoft Learn<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/powershell-parameter-validation/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
lists every validation attribute.</p>
<h3 id="scheduled-run-stops-on-a-missing-value">Scheduled run stops on a missing value<a class="heading-anchor" href="https://looselydecoupled.com/powershell-parameter-validation/#scheduled-run-stops-on-a-missing-value"
     aria-label="Link to section: Scheduled run stops on a missing value" data-pagefind-ignore></a>
</h3>
<p>A mandatory parameter prompts when someone is at the console. Started with
<code>-NonInteractive</code>, the script stops instead:</p>
<div class="console console--error"><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Error</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">Remove-OldLog.ps1:</span> <span class="console__token">Cannot</span> <span class="console__token">process</span> <span class="console__token">command</span> <span class="console__token">because</span> <span class="console__token">of</span> <span class="console__token">one</span> <span class="console__token">or</span> <span class="console__token">more</span> <span class="console__token">missing</span> <span class="console__token">mandatory</span> <span class="console__token">parameters:</span> <span class="console__token">LogRoot.</span></span></code></pre>
    </div>
  </div></div><p>Start the task with <code>pwsh -NonInteractive -File</code> and pass every mandatory
parameter on its command line.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>PowerShell</dt> <dd>7.6.6</dd></div>
    <div class="tested__row"><dt>OS</dt> <dd>macOS 27.0</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>PowerShell Splatting: Replace Long One-Liners with a Hashtable</title>
      <link>https://looselydecoupled.com/splatting-readability-scaling/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/splatting-readability-scaling/</guid>
      <pubDate>Sun, 12 Nov 2023 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>PowerShell</category>
      <category>Code quality</category>
      
      <description>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.</description>
      <content:encoded><![CDATA[<p>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.</p>
<p>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:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="n">-Uri</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span> <span class="n">-Method</span> <span class="n">Post</span> <span class="n">-Headers</span> <span class="vm">@</span><span class="p">{</span><span class="n">Authorization</span><span class="p">=</span><span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span><span class="p">;</span><span class="n">Accept</span><span class="p">=</span><span class="s1">&#39;application/json&#39;</span><span class="p">}</span> <span class="n">-ContentType</span> <span class="s1">&#39;application/json&#39;</span> <span class="n">-Body</span> <span class="nv">$body</span> <span class="n">-TimeoutSec</span> <span class="mf">30</span></span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p>It works. It&rsquo;s also a line you have to read three times, and not because it&rsquo;s
wrong. It doesn&rsquo;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.</p>
<p>Splatting turns that line into a block you can read at a glance, and it&rsquo;s
worth using less as a convenience than as a discipline.</p>
<h2 id="six-months-later">Six months later<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#six-months-later"
     aria-label="Link to section: Six months later" data-pagefind-ignore></a>
</h2>
<p>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.</p>
<p>Six months later the person reading it doesn&rsquo;t know, even when it&rsquo;s the same
person.</p>
<p>The failure isn&rsquo;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.</p>
<p><strong>A script its own author can&rsquo;t debug after six months has a problem, and so
does every script written the same way.</strong></p>
<h2 id="what-makes-a-line-hard-to-read">What makes a line hard to read<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#what-makes-a-line-hard-to-read"
     aria-label="Link to section: What makes a line hard to read" data-pagefind-ignore></a>
</h2>
<p>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.</p>
<p><strong>Syntax you have to parse while reading.</strong> Where does the list of extensions
end? Is that parenthesis inside the script block or after it?</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Get-ChildItem</span> <span class="n">-Path</span> <span class="n">D:</span><span class="p">\</span><span class="n">Exports</span> <span class="n">-Include</span> <span class="p">*.</span><span class="n">csv</span><span class="p">,*.</span><span class="n">json</span> <span class="n">-Exclude</span> <span class="p">*</span><span class="n">-keep</span><span class="p">.*</span> <span class="n">-Recurse</span> <span class="o">-File</span> <span class="p">|</span> <span class="nb">Where-Object</span> <span class="p">{</span> <span class="nv">$_</span><span class="p">.</span><span class="py">LastWriteTime</span> <span class="o">-lt</span> <span class="p">(</span><span class="nb">Get-Date</span><span class="p">).</span><span class="py">AddDays</span><span class="p">(</span><span class="mf">-14</span><span class="p">)</span> <span class="p">}</span> <span class="p">|</span> <span class="nb">Remove-Item</span> <span class="n">-Force</span></span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p><strong>Density.</strong> One line means one chunk. The brain can&rsquo;t break it into pieces
and check them separately.</p>
<p><strong>Missing context.</strong> Why <code>D:\Exports</code>? Why fourteen days? What makes a file
worth keeping? None of that is in the code; it&rsquo;s in the head of whoever wrote
it, or in a document somewhere else.</p>
<p>Any one of these is manageable. The one-liner stacks all three.</p>
<h2 id="what-splatting-forces">What splatting forces<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#what-splatting-forces"
     aria-label="Link to section: What splatting forces" data-pagefind-ignore></a>
</h2>
<p>Splatting takes two steps: put the command&rsquo;s parameters in a hashtable, then
hand the whole table to the command with <code>@</code> in place of <code>$</code>:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$incident</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Uri</span>         <span class="p">=</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Method</span>      <span class="p">=</span> <span class="s1">&#39;Post&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Headers</span>     <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Authorization</span> <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Accept</span>        <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">ContentType</span> <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Body</span>        <span class="p">=</span> <span class="nv">$body</span>
</span></span><span class="line"><span class="cl">    <span class="n">TimeoutSec</span>  <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="nv">@incident</span></span></span></code></pre></div></div>
  </div>
</div><p>Same call. More lines. Far less to hold in your head, for four reasons.</p>
<p><strong>Every parameter is named and on its own line.</strong> Nothing is positional; nothing
depends on order.</p>
<p><strong>The structure is visible.</strong> 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.</p>
<p><strong>Changing it is safe.</strong> Adding a header is adding a line. Deleting a
parameter is deleting a line. There&rsquo;s no adjacent argument to break.</p>
<p><strong>Comments fit.</strong></p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$incident</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Uri</span>         <span class="p">=</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Method</span>      <span class="p">=</span> <span class="s1">&#39;Post&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Headers</span>     <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c"># Read the token from the vault, never from the script.</span>
</span></span><span class="line"><span class="cl">        <span class="n">Authorization</span> <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Accept</span>        <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">ContentType</span> <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Body</span>        <span class="p">=</span> <span class="nv">$body</span>
</span></span><span class="line"><span class="cl">    <span class="c"># Fail fast; the caller retries.</span>
</span></span><span class="line"><span class="cl">    <span class="n">TimeoutSec</span>  <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>None of this is for the computer. It&rsquo;s for the person reading the script
later — probably you, and if not you, someone who can&rsquo;t ask you.</p>
<h2 id="people-first-computers-second">People first, computers second<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#people-first-computers-second"
     aria-label="Link to section: People first, computers second" data-pagefind-ignore></a>
</h2>
<p>There&rsquo;s an old Unix principle: write programs for people first, computers
second. Not because the computer doesn&rsquo;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.</p>
<p>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&rsquo;re passing.
You can&rsquo;t throw arguments at a cmdlet and hope.</p>
<h2 id="rules-that-are-not-obvious">Rules that are not obvious<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#rules-that-are-not-obvious"
     aria-label="Link to section: Rules that are not obvious" data-pagefind-ignore></a>
</h2>
<p>Splatting is simple until it isn&rsquo;t. These are the parts that trip people up.</p>
<p><strong>It&rsquo;s <code>@params</code> at the call and <code>$params</code> everywhere else.</strong> The hashtable is
a normal variable; only the call site uses <code>@</code>. Writing <code>Invoke-RestMethod $incident</code> passes the whole table as the first positional argument, and the
error message will be about a type conversion, not about splatting.</p>
<p><strong>Switch parameters take a boolean.</strong> <code>Recurse = $true</code>, not <code>Recurse = 'yes'</code>
and not a bare <code>Recurse</code>. A hashtable value has to be a value.</p>
<p><strong>You can mix splatted and explicit parameters.</strong> This is the pattern for a
common base with a per-call difference:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$common</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Headers</span>    <span class="p">=</span> <span class="vm">@</span><span class="p">{</span> <span class="n">Authorization</span> <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">TimeoutSec</span> <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="nv">@common</span> <span class="n">-Uri</span> <span class="s2">&#34;</span><span class="nv">$api</span><span class="s2">/components&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="nv">@common</span> <span class="n">-Uri</span> <span class="s2">&#34;</span><span class="nv">$api</span><span class="s2">/incidents&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>PowerShell merges them. Naming the same parameter in both places is an error,
which is what you want — a silent override would be worse.</p>
<p><strong>You can build the table conditionally.</strong> A one-liner can&rsquo;t express &ldquo;add
this parameter only if the variable is set&rdquo;; a hashtable can:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$copy</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Path</span>        <span class="p">=</span> <span class="nv">$source</span>
</span></span><span class="line"><span class="cl">    <span class="n">Destination</span> <span class="p">=</span> <span class="nv">$target</span>
</span></span><span class="line"><span class="cl">    <span class="n">Recurse</span>     <span class="p">=</span> <span class="vm">$true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$DryRun</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$copy</span><span class="p">[</span><span class="s1">&#39;WhatIf&#39;</span><span class="p">]</span> <span class="p">=</span> <span class="vm">$true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Copy-Item</span> <span class="nv">@copy</span></span></span></code></pre></div></div>
  </div>
</div><p><strong>A function can pass its own parameters straight through.</strong> Inside a
function, <code>$PSBoundParameters</code> is a hashtable of the parameters the caller
supplied, and it splats:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="kd">function</span><span class="w"> </span><span class="nb">Get-RecentLog</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">param</span> <span class="p">([</span><span class="no">string</span><span class="p">]</span><span class="nv">$Path</span><span class="p">,</span> <span class="p">[</span><span class="no">switch</span><span class="p">]</span><span class="nv">$Recurse</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">Get-ChildItem</span> <span class="nv">@PSBoundParameters</span> <span class="n">-Filter</span> <span class="p">*.</span><span class="n">log</span> <span class="o">-File</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Sort-Object</span> <span class="n">LastWriteTime</span> <span class="n">-Descending</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">        <span class="nb">Select-Object</span> <span class="n">-First</span> <span class="mf">10</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
  </div>
</div><p>Whatever the caller passed goes through unchanged, and whatever they didn&rsquo;t
pass isn&rsquo;t mentioned at all. It&rsquo;s the cleanest way to write a wrapper, and
it only exists because splatting does.</p>
<p><strong>Arrays splat positionally.</strong> <code>@('a', 'b')</code> splatted fills parameters by
position, not by name. It&rsquo;s legal and almost never what you want in a script
you intend to read later.</p>
<h2 id="bigger-commands-bigger-differences">Bigger commands, bigger differences<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#bigger-commands-bigger-differences"
     aria-label="Link to section: Bigger commands, bigger differences" data-pagefind-ignore></a>
</h2>
<p>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.</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="n">-Uri</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span> <span class="n">-Method</span> <span class="n">Post</span> <span class="n">-Headers</span> <span class="vm">@</span><span class="p">{</span><span class="n">Authorization</span><span class="p">=</span><span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span><span class="p">;</span><span class="n">Accept</span><span class="p">=</span><span class="s1">&#39;application/json&#39;</span><span class="p">;</span><span class="s1">&#39;X-Request-Id&#39;</span><span class="p">=</span><span class="nv">$requestId</span><span class="p">}</span> <span class="n">-ContentType</span> <span class="s1">&#39;application/json&#39;</span> <span class="n">-Body</span> <span class="nv">$body</span> <span class="n">-TimeoutSec</span> <span class="mf">30</span> <span class="n">-UserAgent</span> <span class="s1">&#39;deploy-bot/2.1&#39;</span> <span class="n">-Proxy</span> <span class="s1">&#39;http://proxy.corp.example:8080&#39;</span> <span class="n">-ProxyUseDefaultCredentials</span> <span class="n">-MaximumRetryCount</span> <span class="mf">3</span> <span class="n">-RetryIntervalSec</span> <span class="mf">5</span></span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p>One command, eleven parameters, one of them a table. Now the same call,
splatted:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$incident</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Uri</span>                        <span class="p">=</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Method</span>                     <span class="p">=</span> <span class="s1">&#39;Post&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Headers</span>                    <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Authorization</span>  <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Accept</span>         <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;X-Request-Id&#39;</span> <span class="p">=</span> <span class="nv">$requestId</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">ContentType</span>                <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Body</span>                       <span class="p">=</span> <span class="nv">$body</span>
</span></span><span class="line"><span class="cl">    <span class="n">TimeoutSec</span>                 <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl">    <span class="n">UserAgent</span>                  <span class="p">=</span> <span class="s1">&#39;deploy-bot/2.1&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Proxy</span>                      <span class="p">=</span> <span class="s1">&#39;http://proxy.corp.example:8080&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">ProxyUseDefaultCredentials</span> <span class="p">=</span> <span class="vm">$true</span>
</span></span><span class="line"><span class="cl">    <span class="n">MaximumRetryCount</span>          <span class="p">=</span> <span class="mf">3</span>
</span></span><span class="line"><span class="cl">    <span class="n">RetryIntervalSec</span>           <span class="p">=</span> <span class="mf">5</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="nv">@incident</span></span></span></code></pre></div></div>
  </div>
</div><p>Longer, and easier to understand, modify and debug. When the proxy changes, the
proxy is one line you can find. <code>MaximumRetryCount</code> and <code>RetryIntervalSec</code> need
PowerShell 7. To run the same call on Windows PowerShell 5.1, you delete those
two lines; nothing around them changes.</p>
<h2 id="when-not-to-splat">When not to splat<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#when-not-to-splat"
     aria-label="Link to section: When not to splat" data-pagefind-ignore></a>
</h2>
<p>Splatting is a tool for commands that are hard to read. Applying it to
commands that aren&rsquo;t adds lines and nothing else.</p>
<p><strong>One or two parameters.</strong> <code>Get-Content 'settings.json'</code> doesn&rsquo;t improve with
a hashtable.</p>
<p><strong>Interactive use.</strong> At the prompt you&rsquo;re the only reader, and you&rsquo;re reading
it right now.</p>
<p><strong>Idioms everyone knows.</strong> <code>Get-ChildItem | Where-Object Name -like '*.log'</code> is
a pattern PowerShell users recognize on sight; splatting it hides the idiom.</p>
<p>The rule I use: <strong>splat when future-you would have to re-read the line to
understand it</strong>. For most scripts that turns out to be most of the
non-trivial calls, and almost none of the trivial ones.</p>
<h2 id="documentation-you-no-longer-need">Documentation you no longer need<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#documentation-you-no-longer-need"
     aria-label="Link to section: Documentation you no longer need" data-pagefind-ignore></a>
</h2>
<p>A splatted call documents itself in two places. The variable name says what
the call is for; the table says how:</p>
<div class="code-block" data-copyable data-copy-label="PowerShell">
  <div class="code-block__head"><span class="code-block__lang">PowerShell</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$openIncident</span> <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Uri</span>         <span class="p">=</span> <span class="s1">&#39;https://status.example.com/api/incidents&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Method</span>      <span class="p">=</span> <span class="s1">&#39;Post&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Headers</span>     <span class="p">=</span> <span class="vm">@</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Authorization</span> <span class="p">=</span> <span class="s2">&#34;Bearer </span><span class="nv">$token</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">Accept</span>        <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">ContentType</span> <span class="p">=</span> <span class="s1">&#39;application/json&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="n">Body</span>        <span class="p">=</span> <span class="nv">$body</span>
</span></span><span class="line"><span class="cl">    <span class="n">TimeoutSec</span>  <span class="p">=</span> <span class="mf">30</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Invoke-RestMethod</span> <span class="nv">@openIncident</span></span></span></code></pre></div></div>
  </div>
</div><p>The one-liner needed a comment above it to say what it did. This doesn&rsquo;t.
Comments still earn their place for <em>why</em> — why this endpoint, why this
timeout —
but <em>what</em> is now in the code, where it can&rsquo;t drift from the code.</p>
<h2 id="readability-compounds">Readability compounds<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#readability-compounds"
     aria-label="Link to section: Readability compounds" data-pagefind-ignore></a>
</h2>
<p>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.</p>
<p>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&rsquo;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.</p>
<p>Splatting is how a script written in one evening stays usable for the next
several years.</p>
<h2 id="references">References<a class="heading-anchor" href="https://looselydecoupled.com/splatting-readability-scaling/#references"
     aria-label="Link to section: References" data-pagefind-ignore></a>
</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting" rel="external noopener" target="_blank">about_Splatting<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/splatting-readability-scaling/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> — the PowerShell documentation, including <code>$PSBoundParameters</code> and positional splatting</li>
<li><a href="http://www.catb.org/~esr/writings/taoup/html/ch01s06.html" rel="external noopener" target="_blank">Basics of the Unix Philosophy<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/splatting-readability-scaling/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a> — Eric Raymond on writing for people first</li>
</ul>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
