skills/powershell/SKILL.md
PowerShell standards covering function shape, VMware PowerCLI, modules, and Pester testing. Sourced from [abix-/powershell-practical](https://github.com/abix-/powershell-practical) (Aluminium module, 10k LOC vSphere automation). Use when writing PowerShell.
npx skillsauth add abix-/claude-blueprints powershellInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Source repo: abix-/powershell-practical/Aluminium
(vSphere / Windows automation module, ~10K LOC). Style is
pragmatic PowerCLI: function-per-file in flat layout,
comment-based help on every cmdlet, PSCustomObject outputs that
pipe cleanly.
Provenance note: Aluminium/vmware-community.ps1 (~1100
lines) hosts community-authored PowerCLI functions (Luc Dekens,
Alan Renouf, Shay Levy, David Pasek, etc.) with credit in the
.NOTES blocks. Those are NOT user style; treat them as
reference implementations only. The rest of the module
(vmware.ps1, vmware-reporting.ps1, vmware-monitoring.ps1,
automation.ps1, internal.ps1) is user-authored. User-authored
PowerShell is roughly 700K bytes, not the 757K GitHub reports.
pwsh) for new work. Cross-platform, pipeline
chain operators (&& / ||), ternary, null-coalescing.$IsWindows /
$PSVersionTable.PSVersion.Major -ge 7 branching unless required.Standard cmdlet:
function Get-DatastoreDetails {
<#
.SYNOPSIS
Returns capacity, provisioned, and overcommit metrics for datastores.
.DESCRIPTION
Filters by name pattern and optional parent. Uses Get-View for
server-side filtering rather than client-side Where-Object.
.PARAMETER Name
Datastore name pattern (wildcards allowed).
.PARAMETER Parent
Optional parent container (cluster, folder, datacenter).
.EXAMPLE
Get-DatastoreDetails -Name "prod-*" -Parent "cluster01"
.NOTES
Returns PSCustomObject with formatted GB values.
#>
[CmdletBinding()]
param (
[string]$Name = "",
[string]$Parent
)
# ...
}
Get-Verb lists them).
Get-, Set-, New-, Remove-, Test-, Invoke-, Find-.[CmdletBinding()] always. Gives -Verbose, -Debug,
-ErrorAction, -WarningAction for free.param ( ... ) block follows [CmdletBinding()]. Type-annotate
parameters with [string], [int], [switch], [hashtable].[Parameter(Mandatory=$true)] for required args.[string]$Name = ""..SYNOPSIS, .DESCRIPTION, .PARAMETER X
(one per param), .EXAMPLE, .NOTES. Help is part of the API.Build typed objects for pipeline-friendly output:
$results = @()
foreach ($_d in ($datastores | Select-Object -ExpandProperty Summary)) {
$capgb = (($_d.Capacity) / 1GB)
$results += [pscustomobject][ordered]@{
Name = $_d.Name
"CapacityGB" = "{0:N2}" -f $capgb
"MaxUsableGB" = "{0:N2}" -f ($capgb * 0.80)
"FreeGB" = "{0:N2}" -f ($_d.FreeSpace / 1GB)
"ProvisionedGB" = "{0:N2}" -f $provgb
}
}
return $results | Sort-Object Name
[pscustomobject][ordered]@{} keeps key order. Without [ordered],
the hashtable randomizes display order."FreeGB") when the column name has special chars or
needs to display as-is."{0:N2}" -f $val for formatted strings. Standard .NET format codes.1GB, 1MB, 1KB. Built-in constants.Format-Table, Export-Csv, ConvertTo-Json as they need.PowerShell's pipeline is the calling convention. Write functions that both accept and emit pipeline objects.
$datastores | Where-Object { $_.FreeGB -lt 100 } | Sort-Object FreeGB | Format-Table
ValueFromPipeline=$true / ValueFromPipelineByPropertyName=$true
on parameters to accept piped input.process { ... } block for per-item handling.Select-Object -ExpandProperty X to flatten one level. Common
Aluminium pattern: $datastores | Select-Object -ExpandProperty Summary.+= on large arrays. Use [System.Collections.ArrayList] or
assign the pipeline directly: $results = foreach ($x in $items) { ... }.try {
$vm = Get-VMHost $_v -ErrorAction Stop
}
catch {
Write-Host "$($_v): VMHost not found" -ForegroundColor Red
continue
}
-ErrorAction Stop forces a non-terminating cmdlet error into a
terminating one that try/catch can see. Without it, the catch
block won't fire.$_ inside catch is the ErrorRecord. $_.Exception.Message for
the message, $_.ScriptStackTrace for the trace.throw "message" to raise; Write-Error for non-terminating.continue inside a loop to keep going past one bad host;
catch and return at the function level to abort.Write-Verbose "[$Name] starting": diagnostic. Only shown with
-Verbose. Default for tracing / progress.Write-Host "$($_v): done" -ForegroundColor Green: user-facing,
ignores -Verbose. Use for status, prompts, and colored markers.Write-Output (or bare emission): goes down the pipeline. NEVER
use it for status; it pollutes the function's return value.Write-Warning for "completed but degraded".Write-Debug requires -Debug and pauses execution; rarely useful.Bracketed prefix in messages ([$($_v)], [$Name]) is the
Aluminium convention. Greppable across long logs.
"$Name: $($obj.Count) items"
"Result: $($vm.Name) is $($vm.PowerState)"
$var expansion works for simple variables.$() subexpression for property access, method calls, indices, or
expressions.`) is PowerShell's escape character, not the shell's
command substitution.foreach ($_v in $vmhosts) { ... }
$vmhosts | ForEach-Object { ... }
foreach (... in ...) for straight iteration. Faster, more
readable for a single loop.| ForEach-Object { ... } when chaining further down the pipeline.
Has $_ as the current item.$_v (host), $_d (datastore),
$_T (table), short lowercase prefixed with _. Pick consistent
short names; the body is short enough that this works.Aluminium/
Aluminium.psd1 # manifest
Aluminium.psm1 # entry module: dot-sources function files
vmware.ps1 # one functional area per file
vmware-reporting.ps1
vmware-tests.ps1
windows-server.ps1
.psd1 (manifest): ModuleVersion, RequiredModules, GUID,
exported function list, dependencies..psm1 (root): typically dot-sources the .ps1 files in the
directory or holds module-level functions..ps1 files, named by area (vmware-reporting.ps1,
vmware-monitoring.ps1). Search-friendly.Export-ModuleMember -Function *-* to expose all Verb-Noun
functions automatically. Set FunctionsToExport in the manifest
for stricter control.# server-side filtering with Get-View is FAST
$datastores = @(Get-View -ViewType Datastore -Filter @{"Name"="$Name"})
# UID parsing to find the originating vCenter
$ParentvCenter = $obj.UID |
Where-Object { $_ -match "@(?<vcenter>.*):443" } |
ForEach-Object { $matches['vcenter'] }
# bulk operations
Get-VMHost $hosts | ForEach-Object {
Set-AdvancedSetting -AdvancedSetting (Get-AdvancedSetting -Entity $_ -Name $Name) -Value $value
}
Get-View over Get-VM / Get-VMHost for large fleets. Returns
view objects you can navigate by MoRef; orders of magnitude
faster than the full PowerCLI cmdlet on 1000+ VMs.-Server parameter explicitly when connected to multiple
vCenters.Connect-VIServer $vc -ErrorAction Stop then
finally Disconnect-VIServer -Confirm:$false.Export-Csv -NoTypeInformation -Path $path. Without
-NoTypeInformation, the first line is a #TYPE header that breaks
most consumers.Aluminium does not currently ship Pester tests, but they are the standard. Layout:
# Get-DatastoreDetails.Tests.ps1
Describe "Get-DatastoreDetails" {
BeforeAll {
. $PSScriptRoot/Get-DatastoreDetails.ps1
Mock Get-View { ... }
}
It "returns one row per datastore" {
$result = Get-DatastoreDetails -Name "prod-*"
$result.Count | Should -Be 3
}
It "rounds to 2 decimals" {
$result = Get-DatastoreDetails -Name "prod-*"
$result[0].CapacityGB | Should -Match '^\d+\.\d{2}$'
}
}
Should -Be, not Should Be).BeforeAll / BeforeEach / AfterAll for setup.Mock replaces cmdlets; pin with -ParameterFilter.*.Tests.ps1 file per function. Co-locate or put under
tests/.Invoke-Pester ./tests or Invoke-Pester -Output Detailed.Get-View over Get-VM / Get-VMHost for large fleets.
Server-side filtering via -Filter @{} is orders of magnitude
faster.+= on arrays in loops. PowerShell arrays are
immutable; += rebuilds the array each time (O(n^2)). Options:
$results = foreach ($x in $xs) { ... }.[System.Collections.Generic.List[object]]: $list = [List[object]]::new(); $list.Add($x).[System.Collections.ArrayList] (legacy but still works).foreach (statement) over ForEach-Object (cmdlet) for very
large collections. Statement is 2-10x faster due to per-item
dispatch overhead. ForEach-Object's process { } block is the
bottleneck.Where-Object {} over ?{} alias semantically identical, but
property-name comparison (Where-Object Name -eq 'foo') skips
the script block parse and is ~2x faster.foreach loop for very large
collections due to per-item dispatch overhead. The pipeline
shines for memory (it streams) but loses on CPU for in-memory data.[System.IO.File]::ReadAllLines($p)
beats Get-Content by 5-10x. Cmdlets have parameter binding
overhead per call.Write-Host in loops. Each call hits the host UI;
redirect to a list and emit once.ForEach-Object -Parallel { } -ThrottleLimit N (PowerShell 7
only). Spawns N runspaces. Each iteration runs in its own
context; $using:var to capture outer variables.
$vms | ForEach-Object -Parallel {
Get-VM $_ | Select-Object Name, PowerState
} -ThrottleLimit 10
Start-ThreadJob for fire-and-forget background work.
Cheaper than Start-Job (which uses a separate process).PoshRSJob module wraps this.Measure-Command { ... } for one-shot timing. Returns
TimeSpan. Wrap the entire pipeline.Trace-Command for deep-trace into cmdlet internals:
Trace-Command -Name ParameterBinding -Expression { Get-Process } -PSHost.Get-Counter for OS-level perf counters.Microsoft.PowerShell.Utility/Get-Random: avoid in tight loops;
the cryptographic implementation is slow.1..10 | ForEach-Object { (Measure-Command { ... }).TotalMilliseconds } |
Select-Object -Skip 1 | Measure-Object -Average -Minimum -Maximum
$Verbose switches named like [switch]$Verbose.function,
if, foreach, try.gci, ?, %). Spell out the cmdlet.
Aliases are fine in the REPL.Write-Host for data. It bypasses the pipeline.+= on large arrays.Invoke-Expression. Almost always wrong; injection-prone.$ErrorActionPreference = "SilentlyContinue" at script scope.
Use -ErrorAction per-call when you mean it.-WhatIf run first.Read-Host in non-interactive runs. The harness errors out.C:\Scripts. Use $PSScriptRoot or
Join-Path.development
YAML standards for config files, Ansible playbooks, k8s manifests, GitHub Actions, docker-compose, and any project config. Built from the YAML 1.2 spec, yamllint defaults, and the practical pitfalls (Norway problem, type coercion, anchor gotchas).
development
--- name: ueforge description: ueforge framework: the base layer every UE4SS Rust mod in the Grounded2Mods workspace builds on. Authoritative on the composition model (Effect/Trigger/Skill), the Def/Registry/Instance/Controller pattern, hot reload, discovery, hardening doctrine, and the five framework modules (rpg, stacks, difficulty, inventory, damage). Use when writing or modifying code under `ueforge/` in [abix-/Grounded2Mods](https://github.com/abix-/Grounded2Mods), or when promoting a patte
tools
TypeScript and JavaScript standards. Sourced from [abix-/chromium-extensions](https://github.com/abix-/chromium-extensions) (Hush + filter-anything-everywhere). Use when writing TS/JS, including browser extension bootstrap shims, MV3 service workers, and small web frontends.
development
--- name: schedule1 description: Modding Schedule 1 (TVGS, IL2CPP Unity + MelonLoader + Harmony). Authoritative on Schedule 1 game specifics: engine type, MelonLoader/Il2CppInterop references, eMployee mod root-cause findings, vanilla CookRoutine + StartMixingStationBehaviour internals, certainty-tracking discipline. Mod code lives in [`abix-/Schedule1Mods`](https://github.com/abix-/Schedule1Mods) (the `EmployeeReset` sidecar is the current shipped mod). Not for playing the game. user-invocable: