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'

Comments#

Single-line comments start with #. Block comments are <# ... #>.

# single-line comment

<#
  block of commentary
  PowerShell will ignore
#>

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

-eq / -ne

Equal / not equal

-lt / -le / -gt / -ge

Less than / less than or equal / greater / greater or equal

-like / -notlike

Wildcard match (* and ?)

-match / -notmatch

Regex match; captures in $matches

-contains / -notcontains

Collection contains a scalar value

-in / -notin

Scalar is a member of a collection

-is / -isnot

Type test ($x -is [int])

-replace

Regex replace ('abc123' -replace '\d+','X')

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 $PROFILE aggressively.

  • 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).