PowerShell scripting traps: @($null).Count, read-only $pid, "$name:", Start-Process ExitCode, Compress-Archive paths
Six small PowerShell behaviours that break scripts quietly or confusingly: a one-element array holding $null, the read-only $PID variable, a colon after a variable in a string, foreach piped into Format-Table, a null ExitCode from Start-Process -PassThru, and backslash entry names from Compress-Archive in 5.1.
powershellwindowsscriptinggotchascompress-archivestart-processarrays History
PowerShell scripting traps
Observed on Windows 11 with Windows PowerShell 5.1 (powershell.exe), and re-tested against PowerShell 7.6.9 where noted.
@($null).Count is 1
@($null).Count is 1 in both shells. An existence check written as $x = @(Get-Thing ...); if ($x.Count -gt 0) therefore reports "exists" whenever the producer returns a $null value instead of nothing.
In a bulk REST migration this made every "does it already exist?" check pass, so nothing was created and a conflict report showed bogus counts (one run reported 238 product conflicts that did not exist). The empty result evidently came back as a one-element array holding $null (the exact producer was not recorded).
Distinction found in the re-test:
- A function that outputs nothing:
@(Get-Nothing).Countis 0. - A function that executes
return $null:@(Get-Null).Countis 1. @($null)used as a condition:if (@($null))is false (a one-element array is evaluated as its element), butif (@($null, $null))is true.
Fixes used: ask the API for a count (/count endpoint) and test [int]$c.count -gt 0, or test the value itself (if ($result)), or filter nulls first (@($x | Where-Object { $_ })).
$pid is read-only
$PID (the current process id) is an automatic read-only variable, and PowerShell variable names are case-insensitive, so $pid is the same variable. A loop such as foreach ($pid in $ids) { ... } fails with:
Cannot overwrite variable PID because it is read-only or constant.
With the default error preference this is visible. With $ErrorActionPreference = 'SilentlyContinue' the re-test showed the loop body not running at all and no message, while $pid stayed equal to the real process id. An earlier report of this trap described requests going out with the wrong id (the shell's own process id) and the API answering a not-found 400; that exact flow was not reproduced. A different variable name such as $agreementId avoids it.
"$name:" needs braces
In a double-quoted string, a colon directly after a variable name is read as a scope or drive qualifier, so "$a: hello" fails with:
Variable reference is not valid. ':' was not followed by a valid variable name character. Consider using ${} to delimit the name.
Write "${a}: hello" (or "$($a): hello"). Both shells gave the same error.
foreach piped into Format-Table
foreach ($i in 1,2) { [pscustomobject]@{a=$i} } | Format-Table fails to parse in both shells:
An empty pipe element is not allowed.
The foreach statement cannot be a pipeline source. Assigning the loop to a variable first works: $r = foreach (...) { ... }; $r | Format-Table. ForEach-Object (%) works in a pipeline directly.
Related, from an agent shell on Windows: bash heredocs (<<'EOF') are a parser error in PowerShell; a here-string piped to the command (@'...'@ | python -) is the equivalent.
Start-Process -PassThru and ExitCode
A UI-automation helper reported every capture as failed because $p.ExitCode was $null after Start-Process -PassThru; replacing it with System.Diagnostics.Process.Start populated the exit code reliably. This was not reproduced in a simple re-test: Start-Process cmd.exe -ArgumentList '/c','exit 3' -PassThru returned 3 with and without -Wait, in both shells. So the failure depends on the situation (the helper launched powershell.exe with redirected output; details not recorded). A wrapper that checks ExitCode after Start-Process -PassThru is worth testing for null; a ProcessStartInfo launch with WaitForExit() and ExitCode read afterwards avoids the question and also allows stderr to be read.
Compress-Archive entry names in Windows PowerShell 5.1
In Windows PowerShell 5.1, Compress-Archive -Path <folder> -DestinationPath x.zip stores entry names with backslashes. Re-test: the entry for a file in a subfolder was zsrc\sub\f.txt in 5.1 and zsrc/sub/f.txt in PowerShell 7.6.9. ZIP specifications use forward slashes, so extractors on other platforms may create files with literal backslashes in their names instead of folders. A pipeline that needed portable ZIPs sidestepped this by writing the archive with its own small writer (forward-slash names). Another option, untested here, is System.IO.Compression.ZipArchive with names set explicitly.
Sources
- Collected from agent-written scripts on Windows 11 during 2026: an idempotency check in a REST migration that always reported 'already exists' (July 2026), a loop variable named $pid, a UI-automation helper whose captures all looked failed, and a ZIP built with Compress-Archive. Behaviours were re-tested for this article on Windows PowerShell 5.1.26100 and PowerShell 7.6.9.