PowerShell 7 Skill Baseline
Project-agnostic PowerShell 7 patterns for reliable, performant, cross-platform scripts and modules. Optimized for AI-assisted code generation with progressive disclosure — core patterns inline, detailed references loaded on demand.
When to Use This Skill
- Writing or reviewing PowerShell 7 functions, scripts, or modules
- Generating Pester 5+ tests for PowerShell code
- Optimizing performance (collections, strings, lookups)
- Building cross-platform PowerShell (Windows, Linux, macOS)
- Configuring PSScriptAnalyzer rules
- Migrating from Windows PowerShell 5.1 to PowerShell 7
Bundled Resources
Load these on demand when deeper context is needed:
| Resource | Path | Use When |
|---|---|---|
| Performance benchmarks | references/performance.md |
Optimizing collections, strings, objects, file I/O |
| Error handling tiers | references/error-handling.md |
Implementing typed exceptions, resource cleanup |
| Cross-platform guide | references/cross-platform.md |
Path handling, platform detection, OS differences |
| Module development | references/module-development.md |
Creating manifests, loaders, publishing modules |
| Pester 5 testing | references/testing.md |
Test structure, mocking, assertions, organization |
| Anti-patterns & migration | references/anti-patterns.md |
Code review, 5.1→7 migration checklist |
| PSScriptAnalyzer config | templates/PSScriptAnalyzerSettings.psd1 |
Drop-in analyzer configuration |
1. Function Template (CRITICAL)
Every function must use [CmdletBinding()] — non-negotiable.
function Get-ConfigData {
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter()]
[ValidateSet('JSON', 'YAML', 'PSD1')]
[string]$Format = 'JSON'
)
process {
try {
if (-not (Test-Path -LiteralPath $Path)) {
throw [System.IO.FileNotFoundException]::new(
"Configuration file not found: $Path"
)
}
$content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
switch ($Format) {
'JSON' { $content | ConvertFrom-Json }
'PSD1' { Import-PowerShellDataFile -LiteralPath $Path }
}
}
catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
}
State-Changing (ShouldProcess)
function Remove-CacheData {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path $_ -PathType Container })]
[string]$CachePath
)
process {
if ($PSCmdlet.ShouldProcess($CachePath, 'Remove all cache files')) {
Remove-Item -Path (Join-Path $CachePath '*') -Recurse -Force
}
}
}
Pipeline (begin/process/end)
function ConvertTo-NormalizedRecord {
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[object]$InputObject,
[ValidateNotNullOrEmpty()]
[string]$Prefix = 'REC'
)
begin { $count = 0 }
process {
$count++
[pscustomobject]@{
Id = '{0}-{1:D4}' -f $Prefix, $count
Name = $InputObject.Name
Value = $InputObject.Value
}
}
end { Write-Verbose "Converted $count records" }
}
Key Rules
- Always
[CmdletBinding()]— enables-Verbose,-ErrorAction, common parameters - Always
[OutputType()]— documents return type for callers and tooling - Validate every parameter —
[ValidateNotNullOrEmpty()],[ValidateSet()],[ValidateRange()],[ValidateScript()],[ValidatePattern()] - Use
SupportsShouldProcessfor any function that modifies state - Pass
-WhatIf:$WhatIfPreferenceexplicitly to called cmdlets
2. Error Handling (CRITICAL)
Full reference:
references/error-handling.md— typed exceptions, ErrorRecord construction, resource cleanup
Essential Pattern (Every Function)
process {
try {
$result = Get-Item -LiteralPath $ResourceId -ErrorAction Stop
$result
}
catch {
$PSCmdlet.ThrowTerminatingError($PSItem)
}
}
Typed Exceptions
throw [System.IO.FileNotFoundException]::new("Config not found: $path")
throw [System.ArgumentException]::new("Invalid format: $format")
try { Get-Resource -ResourceId $id }
catch [System.IO.FileNotFoundException] { Write-Warning "Missing: $($PSItem.Exception.Message)" }
catch { Write-Error "Unexpected: $($PSItem.Exception.Message)" }
Key Rules
-ErrorAction Stopinsidetry— converts non-terminating to terminating$PSCmdlet.ThrowTerminatingError($PSItem)— error points at caller, not internals- Bare
throwre-throws preserving original stack trace - Never empty catch blocks — at minimum log the error
- Include context: what failed, what value, what to do next
3. Performance Quick Reference (CRITICAL)
Full reference:
references/performance.md— complete benchmarks, file processing, additional tips
| Pattern | Fast | Avoid |
|---|---|---|
| Collections | $r = foreach (...) { ... } or List<T>.Add() |
+= @() in loops |
| Strings | -join or StringBuilder |
+= string concat in loops |
| Objects | [pscustomobject]@{} |
New-Object PSObject |
| Lookups | Hashtable $h[$key] — O(1) |
Where-Object per lookup — O(n) |
| Output suppress | $null = expr |
Out-Null in loops |
| File read | [IO.File]::ReadLines() for large files |
Get-Content line-by-line piped |
Collections Benchmark
| Method | 10K items | 100K items |
|---|---|---|
| Direct loop assignment | 1x | 1x |
List<T>.Add() |
~4x | ~124x |
+= operator |
~15x | ~18,000x |
4. Data Structures (CRITICAL)
# Arrays — guarantee array from pipeline
$results = @(Get-ChildItem -Path $dir -Filter '*.log')
[string[]]$names = 'Alice', 'Bob', 'Carol'
# Ordered hashtable
$config = [ordered]@{ Name = 'MyApp'; Version = '1.0.0'; Debug = $false }
# PSCustomObject — fastest creation, preserves property order
$record = [pscustomobject]@{ Id = 1; Name = 'Test'; Status = 'Active' }
# Check property existence (works even if value is $null)
if ($record.psobject.Properties.Match('Status').Count) { <# exists #> }
Array Gotchas: Out-of-bounds returns $null silently. Indexing $null throws RuntimeException. -1 index gets last item. $data[0..-1] does NOT enumerate all items.
5. Cross-Platform (HIGH)
Full reference:
references/cross-platform.md— platform detection, differences table, removed aliases
# ALWAYS: Join-Path for path construction
$configPath = Join-Path $PSScriptRoot 'config' | Join-Path -ChildPath 'settings.json'
# Platform-specific config
$configDir = if ($IsWindows) { Join-Path $env:APPDATA 'MyTool' }
else { Join-Path $HOME '.config' 'mytool' }
Key Rules: Never hardcode \ or /. Module names must match filename case on Linux/macOS. SecureString is NOT encrypted on non-Windows — use SecretManagement. Aliases ls, cp, mv, rm removed on non-Windows.
6. Security (HIGH)
# PSCredential parameter (never hardcode secrets)
param([Parameter(Mandatory)] [PSCredential]$Credential)
# SecretManagement module
$apiKey = Get-Secret -Name 'ApiKey' -Vault 'MyVault'
# Use -LiteralPath for user-provided paths (prevents wildcard injection)
Get-Content -LiteralPath $userProvidedPath
Key Rules: Never Invoke-Expression with user input. Escape regex: [regex]::Escape($userInput). Never interpolate user input into SQL. Use [ValidateSet()], [ValidateRange()], [ValidateScript()] on all parameters.
7. Strings, Control Flow & Pipeline (HIGH)
# String interpolation (double quotes) vs literal (single quotes)
"Hello, $userName" # Expands variable
'No $expansion here' # Literal
"Count: $($processes.Count)" # Subexpression
'User {0} at {1:yyyy-MM-dd}' -f $name, (Get-Date) # Format operator
# $null comparisons — ALWAYS $null on the left
if ($null -eq $value) { <# null #> }
# PS 7+ operators
$status = $isActive ? 'Active' : 'Inactive' # Ternary
$value = $config.Setting ?? 'default' # Null-coalescing
$config.Timeout ??= 30 # Null-coalescing assignment
# Splatting — preferred for 3+ parameters
$params = @{ Path = $source; Destination = $dest; Force = $true }
Copy-Item @params
# Pipeline: pipe at end of line (never backtick continuation)
Get-Process |
Where-Object CPU -GT 100 |
Sort-Object CPU -Descending |
Select-Object -First 10
8. Style & Conventions (MEDIUM)
| Element | Convention | Example |
|---|---|---|
| Functions | Verb-Noun (approved verbs) | Get-UserProfile |
| Parameters | PascalCase | $OutputPath |
| Local variables | camelCase | $itemCount |
| Script scope | $script:PascalCase |
$script:CacheData |
| Constants | UPPER_SNAKE_CASE | $MAX_RETRIES |
Formatting: 4-space indent, OTBS braces, max 115 chars/line, UTF-8 with BOM.
Output streams: Write-Verbose (debug), Write-Information (info), Write-Warning (warn), Write-Error (error). Never Write-Host in module functions.
Aliases: Always full cmdlet names in scripts — Get-ChildItem not gci, Where-Object not ?.
9. PSScriptAnalyzer (MEDIUM)
Ready-to-use config:
templates/PSScriptAnalyzerSettings.psd1
| Severity | Key Rules |
|---|---|
| Error | AvoidUsingPlainTextForPassword, UsePSCredentialType, AvoidConvertToSecureStringWithPlainText |
| Warning | AvoidUsingCmdletAliases, AvoidUsingWriteHost, AvoidUsingPositionalParameters, AvoidUsingInvokeExpression, AvoidUsingEmptyCatchBlock, UseApprovedVerbs, UseShouldProcessForStateChangingFunctions |
Decision Matrix
| Situation | Pattern | Priority |
|---|---|---|
| New function | [CmdletBinding()] + [OutputType()] + param validation |
CRITICAL |
| Building a collection | $r = foreach (...) { ... } |
CRITICAL |
| Error handling | try { -ErrorAction Stop } catch { ThrowTerminatingError } |
CRITICAL |
| User-provided paths | -LiteralPath + Join-Path |
CRITICAL |
| 3+ parameters | Splatting @params |
HIGH |
| State-changing operation | SupportsShouldProcess |
HIGH |
| Secrets/credentials | [PSCredential] or SecretManagement |
HIGH |
| Cross-platform paths | Join-Path (never hardcode separators) |
HIGH |
| String assembly in loops | -join or StringBuilder |
HIGH |
| Creating test file | Mirror source path, Describe/Context/It | HIGH |
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
catch block not triggered |
Missing -ErrorAction Stop |
Add -ErrorAction Stop to cmdlet in try |
| Module functions not exported | FunctionsToExport = '*' or missing |
List functions explicitly in manifest |
| Test can't find function | Missing BeforeAll { . script.ps1 } |
Dot-source the function file in BeforeAll |
| Path fails on Linux | Hardcoded \ separator |
Use Join-Path |
$result.Count returns nothing |
$result is scalar, not array |
Wrap: $result = @(...) |
| Mock not intercepting calls | Function is in different module scope | Use Mock -ModuleName ModuleName |
Related Skills
- pester-test-runner — Run, debug, and manage Pester tests
- invoke-build — Build task management with InvokeBuild
- git-commit — Conventional commit workflow
References
Microsoft Learn — Deep Dives
- Arrays | Hashtables | PSCustomObject
- String Substitution | Exceptions | $null
- If Statement | Switch Statement | ShouldProcess
Microsoft Learn — Performance and Platform
- Performance Considerations | Differences from Windows PowerShell
- Unix Support | PSScriptAnalyzer Rules | Module Manifest Guide