| name | pwsh7-windows |
| description | Windows-only PowerShell cheatsheet for writing, reviewing, debugging, and translating reliable commands and .ps1 scripts. Use whenever you work in PowerShell on Windows, especially for quoting, escaping, interpolation, here-strings, regex and wildcard layers, native executable argument passing, cmd.exe or batch boundaries, text encoding, BOMs, newlines, byte streams, redirection, object pipelines, JSON/CSV/REST serialization, paths, error handling, exit codes, and safe filesystem changes. Trigger on PowerShell, pwsh, PS1, Windows shell commands, or shell syntax that must run under PowerShell. Do not use for WSL, Linux, macOS, or another shell. |
PowerShell 7.6 on Windows — Mistake-resistant cheatsheet
Use this as a guardrail and syntax reference, not as a fixed workflow. Prefer readable commands with
one parser layer, explicit data formats, and observable failures.
Install the dependency
Install the stable PowerShell package with WinGet, open a new terminal, and verify it:
winget install --id Microsoft.PowerShell --exact --source winget
pwsh -NoLogo -Command '$PSVersionTable.PSVersion'
Enforce the runtime boundary
Apply this skill only to Windows and pwsh 7.6.x. Never install, upgrade, uninstall, or repair
PowerShell from this skill. Do not substitute powershell.exe: that is normally Windows PowerShell
5.1 and has materially different encoding and native-argument behavior.
Check version-sensitive work before proceeding:
$version = $PSVersionTable.PSVersion
if (-not $IsWindows -or $PSVersionTable.PSEdition -ne 'Core' -or
$version.Major -ne 7 -or $version.Minor -ne 6) {
throw "Expected PowerShell 7.6.x on Windows; found $($PSVersionTable.PSEdition) $version"
}
Keep Windows-only assumptions explicit. Do not reuse these commands unchanged in WSL or another
shell, even when the executable names look identical.
Default to these rules
- Run automation with
pwsh -NoLogo -NoProfile -NonInteractive. Use -File for a script and
reserve -Command for short, controlled code.
- Use full cmdlet names and named parameters in scripts. Avoid short or ambiguous names such as
%, ?, ls, cat, and curl; add .exe when selecting a native executable.
- Use single-quoted strings unless PowerShell expansion is required. Use a subexpression for
property access inside an expandable string.
- Pass native arguments as an array and splat it. Never construct a command line and send it to
Invoke-Expression.
- Use
-LiteralPath for a concrete path. Use -Path only when wildcard expansion is intentional.
- State the text encoding and newline contract at the file boundary. Use byte APIs for binary data.
- Treat the pipeline as an object pipeline. Apply
Format-* only at the final human-display edge.
- Add
-ErrorAction Stop where a cmdlet failure must enter catch. Check native exit codes
immediately.
- Resolve and validate absolute paths before recursive deletion or movement. Use
-WhatIf first
where supported.
- Test in a clean
pwsh process when profiles, aliases, modules, or session preferences could
change behavior.
Choose the correct invocation shape
| Need | Use |
|---|
| Call a cmdlet or function | Direct invocation with named parameters; use hashtable splatting when long |
| Call an executable synchronously | & $exe @nativeArgs |
Run a .ps1 in the current session | & $scriptPath @scriptArgs |
Run a .ps1 reproducibly in a clean child | pwsh -NoLogo -NoProfile -NonInteractive -File ... |
| Start detached, elevated, in another window, or with redirected files | Start-Process |
| Use a CMD-only built-in or batch file | cmd.exe only at a deliberate, trusted parser boundary |
Do not use Start-Process for an ordinary CLI call. It is asynchronous by default and its
-ArgumentList array is joined back into one space-delimited string.
Quote and escape deliberately
| Intent | Reliable form |
|---|
| Literal text | 'C:\Temp\$name[1].txt' |
| Expand variables | "Hello, $name" |
| Expand a property or expression | "Version: $($PSVersionTable.PSVersion)" |
| Separate a variable from adjacent text or a colon | "${drive}:\data" or "${HOME}: ready" |
Put ' inside a single-quoted string | 'don''t' |
| Put literal double quotes in text | 'say "hello"' |
| Multiline literal | single-quoted here-string, @' ... '@ |
| Multiline expandable text | double-quoted here-string, @" ... "@ |
| Native argument containing spaces | one unquoted array element, such as 'C:\Program Files\App' |
Remember the language boundary:
- Use the backtick, not backslash, as PowerShell's escape character.
- Interpret escape sequences beginning with a backtick (newline, carriage return, tab, and Unicode)
only in double-quoted strings.
- Use
$env:NAME for an environment variable. %NAME% is CMD syntax.
- Use
$(...) for a subexpression. Backticks are not command substitution.
- Use
-eq, -ne, -lt, -gt, -and, and -or. ==, <, and > do not mean the same thing
as in Bash or common programming languages; > redirects output.
Use here-string delimiters on their own lines. Put a newline immediately after the opening marker
and immediately before the closing marker:
$literalJson = @'
{"template":"$name stays literal","quote":"\""}
'@
$message = @"
User: $name
Version: $($PSVersionTable.PSVersion)
"@
Prefer a serializer over handwritten JSON. Use a here-string only for fixed text whose quoting is
already part of the desired payload.
Avoid fragile line continuation
Avoid a trailing backtick. A hidden space after it breaks continuation. Break naturally after a
pipe, comma, binary operator, or opening delimiter. Use hashtable splatting for long parameter sets.
Count parser layers
Treat each of these as a separate grammar: PowerShell, a native program, cmd.exe, regex, wildcard,
JSON, XML, and a remote shell. Reduce layers instead of adding escapes.
- Build objects and call
ConvertTo-Json instead of concatenating JSON.
- Pass an argument array instead of building executable text.
- Use
& to invoke a command stored in a variable.
- Use a scriptblock for PowerShell code already under your control.
- Never pass user-controlled or file-controlled text to
Invoke-Expression.
- Avoid
cmd.exe /c and nested pwsh -Command unless the extra shell is genuinely required.
Distinguish regex, wildcard, and literal matching
$isDate = $text -match '^\d{4}-\d{2}-\d{2}$'
$wrapped = $text -replace '(\w+)', '[$1]'
$safeRegex = [regex]::Escape($literalText)
Select-String -LiteralPath $path -SimpleMatch -Pattern $literalText
Get-ChildItem -Path (Join-Path $root '*.log') # wildcard intentionally enabled
Get-Item -LiteralPath 'C:\data\file[1].txt' # brackets are literal
Use single quotes for regex patterns and replacement strings unless PowerShell interpolation is
intentional. Backslash belongs to regex syntax; it is not PowerShell's escape character.
Pass native arguments without reparsing
Resolve ambiguous executable names and include .exe when an alias or cmdlet could shadow them:
Get-Command -Name where -All
$exe = (Get-Command -Name 'example.exe' -CommandType Application -ErrorAction Stop).Source
$nativeArgs = @(
'--input'
'C:\Path With Spaces\input.txt'
'--label'
'literal $ and "quotes"'
)
$output = & $exe @nativeArgs
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "$exe failed with exit code $exitCode"
}
Do not add quote characters around an array element merely because it contains spaces. PowerShell
7.6 performs the required native argument quoting. Add literal quote characters only when the target
program requires quote characters as part of the argument value.
On Windows, $PSNativeCommandArgumentPassing defaults to Windows. It uses modern argument passing
except for legacy targets including cmd.exe, cscript.exe, wscript.exe, several known tools,
and scripts ending in .bat, .cmd, .js, .vbs, or .wsf. Do not change this preference
globally. If reproducing a known legacy requirement, scope any change inside a scriptblock.
Treat batch and CMD boundaries as injection-sensitive. PowerShell ultimately gives batch arguments
to cmd.exe as a raw command line. Never interpolate untrusted input into a batch command. Prefer a
direct executable or API that accepts discrete arguments.
Use -- according to the receiving command:
- For a PowerShell command,
-- ends PowerShell parameter recognition.
- For a native command, PowerShell passes
-- through; only use it if that program defines it.
- Use
--% only as a last resort for a fixed Windows native command. It stops PowerShell parsing
until a newline or pipe, expands only %ENVIRONMENT_VARIABLE% forms, prevents PowerShell
variables and line continuation, and passes redirection characters literally.
Use Start-Process only when its process controls are required:
$process = Start-Process -FilePath $exe -ArgumentList $trustedArgumentString -Wait -PassThru
if ($process.ExitCode -ne 0) {
throw "Process failed with exit code $($process.ExitCode)"
}
Its -ArgumentList array does not preserve an argv array; PowerShell joins it with spaces. Supply
one correctly quoted string and only trusted values, or use direct & invocation.
Trace unexpected native binding with
Trace-Command -Name ParameterBinding -PSHost -Expression { & $exe @nativeArgs }.
Make encoding and newline choices explicit
PowerShell 7.6 defaults text output to UTF-8 without BOM. Preserve that default explicitly at durable
or interoperable boundaries.
| Data contract | Use |
|---|
| Normal PowerShell 7 / cross-platform text | utf8NoBOM |
| Consumer explicitly requires a UTF-8 BOM | utf8BOM |
| Known current Windows ANSI code page | ansi |
| Known OEM/console code page | oem |
| Raw binary | [IO.File]::ReadAllBytes() / WriteAllBytes() or native byte redirection |
| Structured data | Its serializer: Export-Csv, ConvertTo-Json, or Export-Clixml |
Read the intended unit:
$text = Get-Content -LiteralPath $path -Raw -Encoding utf8
$lines = @(Get-Content -LiteralPath $path -Encoding utf8)
$bytes = [IO.File]::ReadAllBytes($path)
Without -Raw, Get-Content returns line objects and removes line terminators. Do not use a text
round trip for arbitrary bytes.
Use -NoNewline for an exact string; omit it to emit one platform newline per input item:
Set-Content -LiteralPath $path -Value $text -Encoding utf8NoBOM -NoNewline
$lines | Set-Content -LiteralPath $path -Encoding utf8NoBOM
$utf8NoBom = [Text.UTF8Encoding]::new($false)
$crlfText = ($lines -join "`r`n") + "`r`n"
[IO.File]::WriteAllText($path, $crlfText, $utf8NoBom)
Inspect the first bytes when a BOM matters:
$head = [IO.File]::ReadAllBytes($path) | Select-Object -First 4
($head | ForEach-Object { $_.ToString('X2') }) -join ' '
Recognize UTF-8 BOM as EF BB BF, UTF-16LE as FF FE, and UTF-16BE as FE FF.
Treat BOM-less legacy files as ambiguous. PowerShell 7 assumes UTF-8, but an older producer may have
written ANSI or OEM text. Obtain the producer's contract before rewriting it.
Append without corrupting a file
- Use the same encoding as the existing file.
Add-Content detects a BOM; for a BOM-less file in PowerShell 7 it falls back to UTF-8.
Out-File -Append and >> do not detect the existing encoding. They use their configured
default, which can create a mixed-encoding file.
- Avoid append when the encoding is unknown. Read with the known encoding and rewrite intentionally.
Separate text formatting from bytes
- Use
Out-File or > for human-readable, formatted PowerShell output only. Formatting width can
truncate table data.
- Use
Export-Csv, JSON, CLIXML, or direct properties for machine-readable output.
- In PowerShell 7.4 and later, redirecting native stdout directly with
> preserves its bytes.
- Do not merge native stderr with stdout when byte preservation matters;
2>&1 converts the combined
streams to string data.
$OutputEncoding controls text piped into native applications. It does not control cmdlet or
redirection file encoding.
chcp 65001 changes a console code page; it does not set Set-Content, Out-File, CSV, or JSON
encoding.
Save PowerShell-7-only scripts as UTF-8 without BOM. Use UTF-8 with BOM only when the same script must
also be read by Windows PowerShell 5.1 and contains non-ASCII characters.
Handle paths and filesystem changes safely
Build paths with Join-Path, validate them with Test-Path -LiteralPath, and canonicalize existing
items with Resolve-Path -LiteralPath. Remember:
C:\name is rooted; C:name is relative to the current location on drive C:.
- PowerShell accepts
/ for provider paths, but a native Windows program may require \.
- PowerShell does not search the current directory for executables; use
.\tool.exe or a full path.
Resolve-Path requires an existing item. For a new destination, resolve its existing parent and
join the leaf name.
- Use
-LiteralPath for names containing [, ], *, or ?.
- Treat provider paths such as
Registry::, Cert:, and Env: separately from filesystem paths.
Before recursive delete or move, reject the root itself and prefix collisions:
$root = (Resolve-Path -LiteralPath $workspaceRoot -ErrorAction Stop).Path.TrimEnd('\')
$targetItem = Get-Item -LiteralPath $candidate -Force -ErrorAction Stop
$target = $targetItem.FullName.TrimEnd('\')
$comparison = [StringComparison]::OrdinalIgnoreCase
$prefix = $root + [IO.Path]::DirectorySeparatorChar
if ($target.Equals($root, $comparison) -or -not $target.StartsWith($prefix, $comparison)) {
throw "Refusing recursive operation outside or at workspace root: $target"
}
if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Refusing recursive operation on a reparse point: $target"
}
Remove-Item -LiteralPath $target -Recurse -Force -WhatIf
# Remove -WhatIf only after reviewing the exact resolved target.
Inspect nested junctions and symbolic links before recursion. Do not assume a textual path prefix is
enough when reparse points can redirect traversal.
Preserve objects until the output boundary
Filter and select properties before formatting:
$services = @(Get-Service | Where-Object { $_.Status -eq 'Running' } |
Select-Object -Property Name, DisplayName, Status)
$services | Export-Csv -LiteralPath $csvPath -Encoding utf8NoBOM -NoTypeInformation
Never pipe Format-Table or Format-List into Export-Csv, ConvertTo-Json, or business logic.
Those cmdlets emit formatting instructions rather than the original objects.
Account for PowerShell's output behavior:
- Wrap possibly empty or scalar results in
@(...) when later code requires an array.
- Put
$null on the left in null checks: if ($null -eq $value).
- Remember that
$collection -eq $value returns matching elements, while
$scalar -eq $value returns a Boolean.
- Use
-contains with the collection on the left or -in with the item on the left.
- Capture unwanted success output with
$null = ... or Out-Null. Every uncaptured value emitted
by a function becomes function output, including output before return.
- Avoid repeated
+= on large arrays; stream results or use a generic list.
Use hashtable splatting for long cmdlet calls:
$request = @{
Uri = $uri
Method = 'Post'
ContentType = 'application/json; charset=utf-8'
Body = ConvertTo-Json -InputObject $payload -Depth 10 -Compress
ErrorAction = 'Stop'
}
$response = Invoke-RestMethod @request
Do not pass a hashtable as a JSON request body without serializing it. Depending on the method,
Invoke-RestMethod can treat a dictionary as form or query data.
Serialize arrays and nested objects deliberately:
$json = ConvertTo-Json -InputObject @($items) -Depth 10 -Compress
$json | Set-Content -LiteralPath $jsonPath -Encoding utf8NoBOM -NoNewline
$data = Get-Content -LiteralPath $jsonPath -Raw -Encoding utf8 | ConvertFrom-Json
ConvertTo-Json defaults to depth 2; always choose a depth that covers the payload. In 7.6, use
-AsArray when even one scalar input must be represented as a JSON array. Use
ConvertFrom-Json -NoEnumerate to preserve a one-element JSON array during a round trip, and
-AsHashtable for empty keys or keys that differ only by case.
Assign Invoke-RestMethod output before enumerating it. A returned array is sent down a direct
pipeline as one Object[]; iterate deterministically with foreach ($item in @($response)).
Make failure observable
Convert cmdlet failures that must abort into terminating errors:
try {
$content = Get-Content -LiteralPath $path -Raw -Encoding utf8 -ErrorAction Stop
} catch {
throw "Failed to read '$path': $($_.Exception.Message)"
}
Native programs do not participate in PowerShell's error system by default. A nonzero exit sets
$? to false and $LASTEXITCODE, but does not create an error record or enter catch. Capture the
exit code before running another native program.
Use a reusable wrapper when zero is the only success code:
function Invoke-NativeCommand {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $FilePath,
[string[]] $ArgumentList = @(),
[int[]] $SuccessExitCode = @(0)
)
& $FilePath @ArgumentList
$exitCode = $LASTEXITCODE
if ($exitCode -notin $SuccessExitCode) {
throw "Native command '$FilePath' failed with exit code $exitCode"
}
}
Learn each tool's exit-code contract; some Windows tools use nonzero informational success codes.
Prefer explicit checking. Enable $PSNativeCommandUseErrorActionPreference = $true only in a narrow
scope where every invoked tool follows the expected convention.
Do not use $? as durable state. Any subsequent operation can overwrite it. Do not expect
$ErrorActionPreference = 'Stop' to handle native exit codes unless
$PSNativeCommandUseErrorActionPreference is deliberately enabled.
Start scripts from a robust skeleton
#requires -Version 7.6
#requires -PSEdition Core
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $InputPath
)
Set-StrictMode -Version 3.0
$ErrorActionPreference = 'Stop'
if (-not $IsWindows) {
throw 'This script supports Windows only.'
}
$version = $PSVersionTable.PSVersion
if ($version.Major -ne 7 -or $version.Minor -ne 6) {
throw "This script targets PowerShell 7.6.x; found $version"
}
$resolvedInput = (Resolve-Path -LiteralPath $InputPath -ErrorAction Stop).Path
[pscustomobject]@{
InputPath = $resolvedInput
PowerShellVersion = $version.ToString()
Succeeded = $true
}
Add SupportsShouldProcess and call $PSCmdlet.ShouldProcess() for mutating advanced functions.
Use $PSScriptRoot, not $PWD, for files located relative to a script.
Validate before handoff
Parse a script without executing it:
$tokens = $null
$parseErrors = $null
$resolvedScript = (Resolve-Path -LiteralPath $scriptPath -ErrorAction Stop).Path
[System.Management.Automation.Language.Parser]::ParseFile(
$resolvedScript, [ref] $tokens, [ref] $parseErrors) | Out-Null
if ($parseErrors.Count -gt 0) {
throw "PowerShell parser found $($parseErrors.Count) error(s)"
}
Then test proportionally:
- Run the script in a fresh
pwsh -NoLogo -NoProfile -NonInteractive -File ... process.
- Exercise paths containing spaces, brackets, apostrophes, Unicode, and a trailing backslash.
- Exercise
$null, zero results, one result, and multiple results.
- Test cmdlet failure and native nonzero exit separately.
- Inspect output bytes with
Format-Hex or [IO.File]::ReadAllBytes().
- Round-trip JSON/CSV and compare properties, not formatted display text.
- Run
Invoke-ScriptAnalyzer only if it is already provisioned; do not install it from this skill.
Use the bundled PowerShell 7.6 reference
The complete official PowerShell-Docs/reference/7.6 snapshot is under
references/7.6/. Open only the pages relevant to the current issue.
| Topic | Start with |
|---|
| Parsing and escaping | about_Parsing, about_Quoting_Rules, about_Special_Characters |
| Splatting and pipelines | about_Splatting, about_Pipelines |
| Encoding and redirection | about_Character_Encoding, about_Redirection, Set-Content, Out-File |
| Native calls and failures | about_Preference_Variables, about_Automatic_Variables, about_Error_Handling, about_Command_Precedence, Start-Process, Trace-Command |
Search the large snapshot from the skill directory:
rg -n -g '*.md' 'PSNativeCommandArgumentPassing|stop-parsing' .\references\7.6
rg -n -g '*.md' 'utf8NoBOM|byte-stream|Encoding' .\references\7.6
# Fallback when rg isn't available
Get-ChildItem -LiteralPath .\references\7.6 -Filter *.md -Recurse |
Select-String -Pattern 'LiteralPath|wildcard'
Treat installed command help as the final machine-specific check:
Get-Help about_Parsing
Get-Help Set-Content -Full
Get-Command -Name $commandName -All
The documentation snapshot is redistributed under CC BY 4.0. See
POWERSHELL-DOCS-LICENSE.md and
POWERSHELL-DOCS-THIRD-PARTY-NOTICES.md.