Overview#
PowerShell is a command interpreter that emits and consumes typed .NET objects. Programs are written as a sequence of statements separated by newlines or semicolons. The interpreter parses each statement, binds parameters by name, runs the cmdlet, captures the output objects, and forwards them down the pipeline.
As a language, PowerShell sits between a shell and a general- purpose .NET language. It has classes, interfaces, real types, nullable variables, parameter validation attributes, and full access to the .NET base class library, alongside the verbose-but- discoverable Verb-Noun cmdlet vocabulary. The right mental model is a typed automation language with a shell as its prompt, not a Unix shell with extras.
The page is grouped by stage. Foundation covers the lexical
surface. Operations covers operators and string handling.
Flow covers control flow, functions, and the advanced-function
template. Wiring covers the object pipeline. Failure
covers exceptions, -ErrorAction, and trap. Re-use is
modules. Runtime is what PowerShell is underneath.
For the variable types (string, ints, arrays, hashtables,
PSCustomObject, classes) and the .NET types under them see
Structures.
Syntax#
A script begins with the .ps1 extension and is run by name or
through pwsh -File:
# script.ps1
Write-Output 'hello'
$ pwsh -File ./script.ps1
$ pwsh -Command 'Get-Process | Select-Object -First 5'
Keywords#
Reserved words include:
begin break catch class continue data define do dynamicparam
else elseif end enum exit filter finally for foreach from
function hidden if in inlinescript param parallel process
return static switch throw trap try until using var while
workflow
Identifiers#
Variable names start with $, then a letter or underscore,
then letters, digits, or underscores. Names are case-insensitive
($Name and $name are the same variable). Special scope
prefixes ($global:, $script:, $private:, $env:)
target a specific scope.
$myVar = 1
$env:PATH # the PATH environment variable
$script:counter = 0 # scope to the script, not the function
Literals#
Strings use single or double quotes. Single quotes are
literal; double quotes expand $var and subexpressions
$(...). Multiline strings use here-strings (@'...'@
literal, @"..."@ expanded). Numerics include decimal,
hex (0x), and scientific notation, plus the type suffixes
KB, MB, GB, TB, PB.
'literal $HOME' # literal
"expanded $HOME" # /Users/operator
"expanded $(Get-Date -Format 'yyyy-MM-dd')"
@'
here-string, no expansion
'@
@"
here-string, $env:USER expands
"@
0xff # 255
1MB # 1048576
Expressions#
A simple command is a cmdlet name followed by parameters.
Parameters are bound by name (-Path C:\tmp) or by
position (Get-Content C:\tmp\log.txt binds to
-Path). The pipeline operator | forwards output. Group
multiple statements with { ... } (script block) or
( ... ) (subexpression).
Get-Process -Name pwsh
Get-Process pwsh # positional binding
$count = (Get-Process | Measure-Object).Count
{ $x = 1; $x + 2 } | ForEach-Object { Invoke-Command -ScriptBlock $_ }
Operators#
PowerShell operators are dash-prefixed (-eq, -like,
-match, -and, -bor). The conventional =, <,
>, &&, || are reserved for assignment, redirection,
and (in 7+) chaining; the comparison forms live behind the
dash to avoid collision.
1 -eq 1 # True
'abc' -like 'a*' # True (wildcard)
'abc' -match '^a' # True (regex)
$true -and $false # False
Test-Path C:\tmp && Get-ChildItem C:\tmp # 7+ chain operator
$cached ?? (Compute-Value) # 7+ null-coalesce
Comparison#
Comparisons are case-insensitive by default. Prefix with c
for case-sensitive (-ceq) or i for explicit case-
insensitive (-ieq).
Operator |
Effect |
|---|---|
|
Equal / not equal |
|
Less than / less than or equal / greater / greater or equal |
|
Wildcard match ( |
|
Regex match; captures in |
|
Collection contains a scalar value |
|
Scalar is a member of a collection |
|
Type test ( |
|
Regex replace ( |
Logical#
-and, -or, -xor, -not (or !). Short-circuit
in 7+ with && and ||.
if ($x -and $y) { ... }
Test-Path $path || throw "missing" # 7+
Assignment#
= assigns. Compound assignment for arithmetic and string
concatenation is the same as C (+=, -=, *=, /=,
%=).
$name = 'operator'
$count = 0
$count += 1
Type Cast#
A bracketed type name in front of an expression coerces it.
[int]'42' # 42
[datetime]'2026-05-15' # DateTime object
[int[]]@('1','2','3') # int array
Strings#
Strings expand variables and subexpressions inside double quotes.
Methods come from the .NET String type (ToUpper,
Substring, Replace, Split, Trim, Contains,
StartsWith, EndsWith).
$name = 'alice'
"hello, $name" # hello, alice
"today is $((Get-Date).ToString('yyyy-MM-dd'))"
$name.ToUpper() # ALICE
$name.Substring(0, 3) # ali
$name -replace 'a', 'A' # AliCe (regex)
'a,b,c'.Split(',') # @('a','b','c')
Format strings are the .NET composite format. -f is the
operator form.
"user={0} count={1}" -f $name, 42
'{0:N2}' -f 3.14159 # 3.14
Regex captures land in $matches after a successful
-match. Named groups expose names.
if ('user42@example.com' -match '^(?<user>[a-z]+)(?<id>\d+)@(?<host>.+)$') {
"user: $($matches.user)"
"id: $($matches.id)"
"host: $($matches.host)"
}
Control#
PowerShell branches and loops by truthiness: $null, 0,
'', empty arrays, and $false are falsy. if /
elseif / else, switch, while, do .. while,
do .. until, for, foreach, plus the pipeline forms
ForEach-Object and Where-Object.
If#
if (Test-Path config.toml) {
'found'
} elseif ($null -eq $args[0]) {
'no argument'
} else {
"got $($args[0])"
}
flowchart LR
C{cond?} -->|true| T[then block] --> X([end])
C -->|false| E[else block] --> X
For#
C-style counter.
for ($i = 0; $i -lt 10; $i++) {
$i
}
Foreach#
Two distinct forms. foreach (the keyword) iterates a
collection in memory. ForEach-Object (the cmdlet, often
aliased %) runs in the pipeline and streams.
foreach ($f in Get-ChildItem *.log) {
$f.Name
}
Get-ChildItem *.log | ForEach-Object { $_.Name }
# 7+ parallel
$hosts | ForEach-Object -Parallel { ssh $_ uptime } -ThrottleLimit 8
While / Until#
while (-not (Test-Path ready.flag)) {
Start-Sleep 1
}
do {
Start-Sleep 1
} until (Test-Path ready.flag)
Switch#
PowerShell switch is unusually capable. It handles regex,
wildcards, file input, and arrays in one statement.
switch ($verb) {
'get' { 'read' }
{ $_ -in 'put','post' } { "write: $_" }
{ $_ -match '^d' } { 'delete-family' }
default { 'unknown' }
}
switch -Regex -File access.log {
'^(\d+\.\d+\.\d+\.\d+)' { $matches[1] }
}
Loop Control#
break exits the innermost loop; continue skips to the
next iteration. return ends the function. In a pipeline
ForEach-Object, continue and break work on the
enclosing language loop, not the pipeline, which trips
beginners up.
Functions#
Two function forms. The basic form uses param(...) for
declaration and behaves like a regular shell function. The
advanced form adds [CmdletBinding()] and begin /
process / end blocks so the function behaves like a real
cmdlet (binding, streaming, -Verbose, -WhatIf).
Basic function.
function Get-LargeFiles {
param(
[string]$Path = '.',
[int]$MinKB = 100
)
Get-ChildItem -Path $Path -Recurse -File |
Where-Object Length -gt ($MinKB * 1KB)
}
Get-LargeFiles -Path . -MinKB 500
Advanced function.
function Set-AppConfig {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$Name,
[ValidateSet('dev','staging','prod')]
[string]$Env = 'dev'
)
process {
if ($PSCmdlet.ShouldProcess($Name, "configure for $Env")) {
# do the work
}
}
}
The process block runs once per pipeline item; begin and
end run once before and after. [CmdletBinding()] unlocks
-Verbose, -Debug, -WhatIf, -Confirm for free.
I/O#
PowerShell distinguishes six streams (Success, Error,
Warning, Verbose, Debug, Information). The pipeline only carries
Success; the others surface to the host (terminal) or get
captured with stream-redirection (2>, 3>, 4>, 5>,
6>). The full surface (every redirection operator, the
pipeline, parallel pipelines, jobs) lives in I/O and Pipelines.
Write-Output 'to success stream' # the default; pipeline carries this
Write-Error 'to error stream'
Write-Warning 'to warning stream'
Write-Verbose 'to verbose stream' -Verbose
Write-Information 'to info stream' -InformationAction Continue
Subexpressions#
$( ... ) evaluates a subexpression and inlines the result.
& { ... } invokes a script block in a child scope. & $cmd
calls a command stored in a variable.
"now is $(Get-Date)"
& { Set-Location /tmp; Get-Location } # scoped cd
Get-Location # parent unchanged
$cmd = 'Get-Process'
& $cmd | Select-Object -First 3
Errors#
Every cmdlet emits terminating errors (exceptions, caught
with try / catch) or non-terminating errors (logged
to the Error stream; do not abort the pipeline). The operator
controls behavior with -ErrorAction per call or
$ErrorActionPreference globally.
try {
Get-Item missing.txt -ErrorAction Stop
}
catch [System.IO.FileNotFoundException] {
"not found: $($_.TargetObject)"
}
catch {
"other error: $_"
}
finally {
'always runs'
}
$ErrorActionPreference = 'Stop' # turn every error into terminating
Strict mode at script top promotes silent bugs to errors.
Set-StrictMode -Version Latest
Modules#
Re-use lives in modules. A module is a folder with a
.psd1 manifest and a .psm1 script, plus any nested
.ps1 / .dll files. Import-Module loads it;
Get-Module lists what is loaded; Find-Module searches the
PowerShell Gallery; Install-Module installs from there.
Import-Module Az.Compute
Get-Module
Find-Module -Name PSScriptAnalyzer
Install-Module -Name Pester -Scope CurrentUser
For one-off code, . ./helpers.ps1 dot-sources a script into
the current scope (functions and variables remain available
after the script returns).
Runtime#
PowerShell is a single-process .NET host. The pwsh
process loads the .NET runtime, parses each command into an AST,
binds it to a cmdlet (or function, or external program), and
runs it in the same process. Cmdlets are .NET classes; the
output objects are .NET objects with full type information.
Practical implications.
Startup is slower than Bash because the .NET runtime has to come up. Profile bloat amplifies it; trim
$PROFILEaggressively.No fork,
ForEach-Object -Parallel(7+) and runspace pools spawn .NET threads; jobs (Start-Job) spawn child processes.Variables are real .NET objects and live in scopes (
global,script,private,local). The default is local-then-walk-up.No tail-call optimization, deep recursion blows the stack.
Library#
PowerShell ships hundreds of built-in cmdlets in the core
modules (Microsoft.PowerShell.Management,
Microsoft.PowerShell.Utility, Microsoft.PowerShell.Core).
Beyond core, the PowerShell Gallery hosts community and
vendor modules (Az, AWS.Tools, Microsoft.Graph,
Pester, PSScriptAnalyzer).
For discovery.
Get-Command -Verb Get -Noun Process # cmdlets matching pattern
Get-Help Get-Process -Examples # docs
(Get-Process)[0] | Get-Member # inspect object shape
Tasks#
Iterate over the lines of a file.
Get-Content input.txt | ForEach-Object { $_ }
Read a file into an array.
$lines = Get-Content input.txt
$lines[0]
Parse CSV.
Import-Csv users.csv |
Where-Object Age -gt 30 |
Select-Object Name, Email |
Export-Csv adults.csv -NoTypeInformation
Parse JSON.
$data = Get-Content data.json | ConvertFrom-Json
$data.users[0].name
Run a step with a timeout.
Start-Job -ScriptBlock { Invoke-RestMethod https://target } |
Wait-Job -Timeout 30 |
Receive-Job
Background work and wait for all jobs.
$hosts | ForEach-Object -Parallel {
ssh $_ uptime
} -ThrottleLimit 8
Capture both output and exit code from a native command.
$out = & ./somebinary 2>&1
$rc = $LASTEXITCODE
References#
Get-Help about_*(the in-shell conceptual help pages;about_Functions,about_Pipelines,about_CommonParameters,about_Operators).PowerShell for the surrounding PowerShell section (Setup, files, PSReadLine, history, completion).
Patterns for
Set-StrictMode,$ErrorActionPreference, the advanced-function template.Tools for the toolchain (
PSScriptAnalyzer,Pester, the language server).The Terminal for the terminal-level view of scope and process nesting.
Microsoft Learn / PowerShell (the upstream manual).
PowerShell Gallery (the module hub).
Comments#
Single-line comments start with
#. Block comments are<# ... #>.