PowerShell — Reference

Source: https://learn.microsoft.com/en-us/powershell/

PowerShell

  • Created: 2006 (Windows PowerShell 1.0) by Microsoft (Jeffrey Snover et al.); open-sourced and rebuilt cross-platform on .NET Core as PowerShell 6.0 in 2018
  • Latest stable: PowerShell 7.4 LTS is built on .NET 8 (latest patch 7.4.15, ~2026); 7.5/7.6/7.7 are in active release; Windows PowerShell 5.1 is the legacy in-box Windows-only edition (no further feature work). (What’s new in 7.4)
  • Paradigms: Object-oriented shell (objects flow through pipelines, not text); imperative; functional sprinkles via script blocks; class-based OO since 5.0
  • Typing: Dynamic; optional static types via [type]$var casts and [CmdletBinding()] parameter attributes; .NET type system underneath
  • Memory: .NET-managed (CLR GC)
  • Compilation: Parsed to AST → bytecode-compiled to dynamic methods at runtime (similar to a JIT script lang); .NET assembly under the hood
  • Primary domains: Windows administration, Microsoft 365 / Azure / Active Directory automation, CI/CD on Windows, infrastructure scripting cross-platform (Linux/macOS), DevOps glue, REST API consumption, package and configuration management
  • Notable runtimes: PowerShell (cross-platform, .NET, current), Windows PowerShell 5.1 (Windows-only, .NET Framework, legacy)
  • Official docs: https://learn.microsoft.com/en-us/powershell/

At a glance

PowerShell’s defining innovation is the object pipeline: instead of streaming text between commands like Unix shells, cmdlets emit and consume .NET objects with typed properties and methods. Get-Process | Where-Object CPU -gt 100 | Sort-Object CPU -Desc | Select-Object -First 5 filters, sorts, and projects without parsing a single line. Cmdlets follow the Verb-Noun convention (Get-, Set-, New-, Remove-, Invoke-, Test-) with a published verb list, which makes discovery (Get-Command *-Process) and tab completion uniform across modules. Two editions coexist: Windows PowerShell 5.1 (in-box on every Windows since 2016, .NET Framework, frozen) and PowerShell 7+ (cross-platform on .NET 8/9, the actively developed line). Many on-box Windows modules still ship only for 5.1 — the Windows Compatibility Module (WindowsPSModulePath / Import-Module -UseWindowsPowerShell) bridges them into 7. (PS docs hub)

Getting started

Install:

Hello world:

# hello.ps1
Write-Output "Hello, world!"

Run: pwsh ./hello.ps1. (Windows: by default scripts are blocked by execution policy — Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.)

Project layout: A module:

MyModule/
  MyModule.psd1            # manifest (metadata, exports, version)
  MyModule.psm1            # script module (or .dll for binary)
  Public/Get-Thing.ps1     # one cmdlet per file (convention)
  Private/Helper.ps1
  Tests/Get-Thing.Tests.ps1  # Pester tests

Package/build tool: PSResourceGet (replacement for legacy PowerShellGet v2; ships with 7.4) — Install-PSResource Pester, Find-PSResource, Publish-PSResource. PowerShell Gallery (https://www.powershellgallery.com/) is the public registry. Module manifests use .psd1 (a restricted PowerShell hashtable). For multi-file modules, Build-Module (from ModuleBuilder) or psake / Invoke-Build for build orchestration.

REPL: pwsh (or powershell.exe for 5.1). With PSReadLine (bundled, 2.3.6+ in 7.4) you get syntax highlighting, history search (Ctrl+R), prediction (Set-PSReadLineOption -PredictionSource HistoryAndPlugin), and ViMode. Windows Terminal is the modern host. VS Code with the PowerShell extension is the IDE.

Basics

Types & literals. Numbers: 42, 0xFF, 1.5, 1.5e3, 1kb/1mb/1gb/1tb/1pb (size suffixes — multipliers, evaluate to [long]), 100ms/1s/1m are NOT supported (use [timespan]::FromSeconds(1)). Strings: single-quoted 'literal' (no expansion), double-quoted "hello $name $($obj.Prop)" (variable + $() subexpression), here-strings @"…multi-linetrue,null. Arrays:@(1, 2, 3)or1, 2, 3. Hashtables:@{a=1; b=2}. Ordered:[ordered]@{…}`.

Variables & scoping.

$name = "alice"               # Untyped
[int]$count = 0               # Typed (cast enforced on subsequent assigns)
[ValidateSet('a','b')] $x = 'a'   # validation attribute
$global:g = 1
$script:moduleVar = 2         # script/module scope
$private:hidden = 3

Scopes: Global, Script, Local, Private. Functions are by-default Local. Modules have an isolated Script scope.

Control flow.

if ($x -gt 0) { … } elseif ($x -lt 0) { … } else { … }
switch ($val) {
    'a' { 'apple' }
    {$_ -is [int]} { 'number' }
    default { 'other' }
}
foreach ($item in $list) { … }
$list | ForEach-Object { … }     # pipeline form (slower per-item, lazier)
for ($i=0; $i -lt 10; $i++) { … }
while (cond) { … }
do { … } while (cond)
do { … } until (cond)

Operators are -eq, -ne, -lt, -gt, -le, -ge, -like (wildcard), -match (regex), -contains, -in, -notin, -band, -bor. Case-sensitive variants: -ceq, -cmatch, etc. (Default is case-insensitive.) == doesn’t exist.

Functions / advanced functions.

function Get-Greeting {
    [CmdletBinding()]                 # makes it an "advanced function" → free -Verbose, -ErrorAction, -WhatIf if [SupportsShouldProcess]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$Name,
        [string]$Greeting = 'Hello'
    )
    process { "$Greeting, $Name!" }
}
'Alice','Bob' | Get-Greeting

begin/process/end blocks for streaming pipeline input (process runs once per input). Without them, all pipeline input collects into $input.

Strings. -f operator for format: '{0,-10} {1:N2}' -f 'pi', 3.14159. .Replace(), .Split(), .Substring(), .PadLeft(). Regex via -match / -replace / -split. Format with Format-Table, Format-List, ConvertTo-Json.

Collections. Arrays via @(…); resize is expensive (allocates new). Use [System.Collections.Generic.List[T]]::new() for repeated Add(). Hashtable: @{}. PSCustomObject (the lightweight record): [PSCustomObject]@{Name='x'; Value=1} — has property access syntax and works in Format-Table automatically.

Intermediate

Type system depth. Full .NET type system: [int], [string], [datetime], [guid], [regex], [scriptblock], [hashtable], [type]. Cast with [type]$x or $x -as [type] (returns $null on failure). Generics: [System.Collections.Generic.Dictionary[string,int]]::new(). Custom enums and classes since 5.0. The PSObject wrapping system lets you attach NoteProperty, ScriptProperty, ScriptMethod to any object at runtime.

Modules. Three flavors:

  • Script module (.psm1) — PowerShell source.
  • Binary module (.dll) — compiled C# implementing Cmdlet / PSCmdlet.
  • Manifest module (.psd1) — points to the others; defines version, dependencies, exports, compatible editions. Auto-loading scans $env:PSModulePath. Export-ModuleMember -Function Get-Thing controls what’s public.

Error handling. Two kinds: terminating (caught by try/catch, fires trap) and non-terminating (collected in $Error[0], doesn’t stop). Cmdlets pick which to emit. Force terminating: -ErrorAction Stop per call, $ErrorActionPreference = 'Stop' globally, or try { … } catch [System.IO.IOException] { … } finally { … }. throw 'msg' raises a terminating error. Write-Error writes a non-terminating one.

Concurrency.

  • ForEach-Object -Parallel { … } -ThrottleLimit 5 (PS7+) — runspace-per-item, true parallel.
  • Start-Job — out-of-process job (heavyweight, fork-equivalent); Wait-Job, Receive-Job, Remove-Job.
  • Start-ThreadJob (in-process, runspace-based — much lighter than Start-Job).
  • Runspaces (the underlying primitive): [runspacefactory]::CreateRunspacePool() for true parallelism with shared variable injection. Foundation of -Parallel.

I/O. Get-Content, Set-Content, Add-Content, Out-File. Encoding default in PS7+ is UTF-8 no BOM; in 5.1 it varies (and historically defaulted to UTF-16 LE with BOM for Out-File). -Encoding utf8, utf8BOM, utf8NoBOM, ascii, unicode. [System.IO.File]::ReadAllText($p) for raw .NET access.

Stdlib highlights. Invoke-WebRequest / Invoke-RestMethod for HTTP (vastly improved in 7.4 — Brotli, persistent connections, custom timeouts). ConvertTo-Json/ConvertFrom-Json. Test-Path, Resolve-Path, Join-Path, Split-Path. Get-ChildItem (ls/dir). Select-String (grep-equivalent). Compare-Object. Group-Object. Measure-Object. Get-Random. Test-Json (now using JsonSchema.NET as of 7.4 — Draft 4 schemas no longer supported, breaking change). (7.4 breaking changes)

Advanced

Memory / GC. .NET CLR GC (server or workstation depending on host). Long-running pwsh instances accumulate variables in $global: scope; clean up with Remove-Variable or use Start-ThreadJob for isolation. Large pipelines stream — they don’t materialize unless you @() or Sort-Object (which buffers).

Concurrency deep dive. Runspaces are the foundation. [powershell]::Create().AddScript($sb).BeginInvoke() queues async work; Wait then EndInvoke to collect. Runspace pools ([runspacefactory]::CreateRunspacePool(1, $maxThreads)) reuse threads — used by ForEach-Object -Parallel. Variables can be injected via $using:varName inside the script block (essential pattern). 7+ added using namespace/module and improved using assembly for binary deps.

FFI. Native interop is just .NET interop:

  • Add-Type -TypeDefinition $csharpSource — compile inline C# at runtime, get a usable .NET type.
  • [DllImport] via Add-Type for raw P/Invoke to native libraries (Win32 APIs, libc).
  • [System.Runtime.InteropServices.Marshal] for buffer/struct marshaling.
  • Windows-only: New-Object -ComObject Excel.Application for COM automation.

Reflection.

  • Get-Member — properties and methods of any object. $obj | Get-Member.
  • Get-Command Get-Process — discovers what command does what.
  • (Get-Command Get-Process).Parameters — full parameter metadata.
  • [type]::GetMethods(), .GetProperties(), .GetFields() — full .NET reflection.
  • Get-PSCallStack — execution stack inspection.

Performance tools.

  • Measure-Command { … } — wall-clock for a script block.
  • Trace-Command — internal tracing of parameter binding, type conversion, etc.
  • PowerShell Profiler: https://github.com/nohwnd/Profiler — sampling profiler for PS scripts.
  • Set-PSDebug -Trace 2 — line-by-line trace.
  • Slow patterns to avoid: array += in a loop (O(n²) — use [List[T]]), ForEach-Object over foreach for large in-memory collections, Select-Object when not projecting (use direct property access).

God mode

  • AST manipulation: [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$tokens, [ref]$errors) returns a full AST. Walk it with $ast.FindAll({ $args[0] -is [CommandAst] }, $true). Foundation of PSScriptAnalyzer, formatters, and the language server.

  • Dynamic modules / [scriptblock]::Create(): Compile arbitrary text into a callable script block at runtime: & ([scriptblock]::Create('Get-Date')). Use sparingly — sandboxing is via Constrained Language Mode (set with $ExecutionContext.SessionState.LanguageMode).

  • Classes (PS5+): Real CLR types from PowerShell:

    class Animal {
        [string]$Name
        Animal([string]$n) { $this.Name = $n }
        [string] Speak() { return 'generic noise' }
    }
    class Dog : Animal {
        Dog([string]$n) : base($n) {}
        [string] Speak() { return 'woof' }
    }

    Attributes work ([ValidateSet], [ValidateRange]). DSC resources are class-based since DSC 2.

  • DSC (Desired State Configuration): Declarative config-as-code; PS5.1 ships PSDesiredStateConfiguration v1; DSC 3 (current) is platform-independent and pulls config from dsc resource/dsc config get. Replaces a lot of Puppet/Chef use cases on Windows. (DSC docs)

  • CIM/WMI: Get-CimInstance Win32_Process is the modern way (DCOM/WSMan, fast). Get-WmiObject is deprecated.

  • .NET reflection from PowerShell: Anything you can do in C# you can prototype in PS — [System.Net.ServicePointManager]::SecurityProtocol, [Reflection.Assembly]::LoadFrom($p), etc.

  • Splatting (@parameters): Pass a hashtable as named parameters: $p = @{Path='c:\x'; Recurse=$true}; Get-ChildItem @p. The single biggest readability win for cmdlets with many params. Splatting an array passes positional args.

  • Advanced functions ([CmdletBinding()] + parameter sets): [Parameter(ParameterSetName='ByName')] and [Parameter(ParameterSetName='ById')] mark params as mutually exclusive sets; PowerShell picks the active set from supplied params. Combine with [ValidateScript({})], [ArgumentCompleter({…})], [ValidatePattern], [ValidateRange].

  • Runspaces + runspace pools: raw API behind every parallel feature. Useful when you need shared state, cancellation tokens, or to inject .NET assemblies into the worker.

  • using statements (top-of-file only):

    • using namespace System.Collections.Generic — alias for shorter type names.
    • using assembly /path/to/lib.dll — load a DLL.
    • using module MyModule — import at parse time (necessary if you reference the module’s classesImport-Module doesn’t expose them).
  • $PSDefaultParameterValues: session-wide defaults: $PSDefaultParameterValues['Get-ChildItem:Force'] = $true. Keys can use wildcards.

  • Custom formatters via .ps1xml: define how your custom types render in Format-Table/Format-List. Update-FormatData -PrependPath my.format.ps1xml.

  • Custom type extensions (.ps1xml): add NoteProperty/ScriptProperty/ScriptMethod to existing types — e.g., add a .ToTable() to every PSCustomObject.

  • JEA (Just Enough Administration): constrained PowerShell endpoints exposing only specific cmdlets/parameters via New-PSSessionConfigurationFile -SessionType RestrictedRemoteServer. Lets help-desk users run sanctioned admin commands without local admin. (JEA docs)

  • PowerShell 7 internals: built on .NET 8 (LTS). pwsh is a native AOT executable + a managed assembly. The pipeline is implemented as Cmdlet.ProcessRecord invocations chained through a PipelineProcessor; objects flow through Pipe.AddInput/Retrieve. Source at https://github.com/PowerShell/PowerShell.

Idioms & style

  • Naming: Verb-Noun, both PascalCase. Use approved verbs (Get-Verb lists them); New-/Add-/Set- have specific meanings. Variables PascalCase or camelCase per personal taste; param names always PascalCase.

  • One pipeline expression per line for readability:

    Get-Process |
        Where-Object CPU -gt 100 |
        Sort-Object CPU -Descending |
        Select-Object -First 5
  • Use [CmdletBinding()] even for trivial functions — gets you common parameters for free.

  • Always use full cmdlet names in scripts. Aliases (? for Where-Object, % for ForEach-Object, gci for Get-ChildItem) are interactive-only.

  • Prefer foreach over ForEach-Object for in-memory collections (no per-item pipeline overhead).

  • Avoid $null -eq $x / $x -eq $null confusion — always put $null on the left: if ($null -eq $x). Right-side $null triggers collection comparison semantics.

  • Splat for >3 params.

  • Use .WriteLine($string) over += on arrays in tight loops.

  • Linter: PSScriptAnalyzer is the standard (Invoke-ScriptAnalyzer .). Editor extensions surface its rules live.

  • Formatter: Invoke-Formatter (also from PSScriptAnalyzer); VS Code’s PowerShell extension uses it.

  • Style guide: community-maintained at https://poshcode.gitbook.io/powershell-practice-and-style/ — adopted widely.

Ecosystem

  • Testing: Pester — BDD framework (Describe, Context, It, Should -Be). Mocking with Mock Get-Foo { … }. Comes preinstalled in 7+.
  • Linting/formatting: PSScriptAnalyzer.
  • Docs: PlatyPS (now Microsoft.PowerShell.PlatyPS, v2) generates Markdown help and external help XML from comment-based help.
  • Packaging: PSResourceGet → PowerShell Gallery.
  • Build orchestration: Invoke-Build, psake.
  • Cross-platform shells: pwsh runs on Linux/macOS for CI portability (GitHub Actions has shell: pwsh available).
  • Notable users: Microsoft (entire Azure CLI shell experience, Microsoft 365 Admin via Exchange Online / Teams / SharePoint Online modules, Intune, Defender), Stack Overflow (deployment), GitHub (Windows-side internal tooling), every Windows shop running Active Directory / Exchange / SCCM / SCOM, OneDrive sync diagnostics scripts, Cloud platform tooling (Az.* modules with hundreds of submodules).

Gotchas

  • The “two PowerShells” problem. Windows PowerShell 5.1 (powershell.exe, .NET Framework, frozen) vs PowerShell 7+ (pwsh, .NET, current). Many built-in Windows modules are 5.1-only — use Import-Module -UseWindowsPowerShell to bridge from 7. The two have different default encodings, different built-in cmdlets, different $PSVersionTable.PSEdition.
  • Default encoding differences. 5.1 Out-File defaults to UTF-16 LE with BOM. 7+ defaults to UTF-8 without BOM. Cross-version scripts must always specify -Encoding utf8.
  • == doesn’t exist. Use -eq. Comparison operators are - prefixed; = is assignment only.
  • Comparison is case-insensitive by default. Use -ceq, -cmatch, -clike for case-sensitive.
  • $null on the right in -eq against an array yields a filter, not a boolean — @(1,2,$null) -eq $null returns @($null). Always if ($null -eq $x).
  • Array += is O(n²). Each += allocates a new array. Use [List[object]]::new().
  • function returns all uncaptured output. function f { 1; 2; 'x' } returns @(1, 2, 'x'). Capture or pipe to Out-Null to suppress.
  • return $x is just shorthand for output-then-return — it doesn’t restrict the function’s output to that value if other expressions also output.
  • Pipeline parameter binding can match unintended params. [Parameter(ValueFromPipelineByPropertyName)] matches pipeline objects’ properties to params by name; can surprise. Inspect with Trace-Command -Name ParameterBinding.
  • Test-Json in 7.4 dropped Draft 4 schema support. Migrate to Draft 7+. (breaking changes 7.4)
  • Execution policy is a Windows-only nag, not security. It blocks scripts by default but anyone can Set-ExecutionPolicy -Scope Process Bypass or pipe into stdin.
  • Start-Process -Wait blocks but doesn’t capture output — use Start-Process -RedirectStandardOutput tmp.txt -Wait or call the executable directly (& foo.exe args does what you want and captures).
  • Native command stderr. & foo.exe 2>&1 works but the items in the merged stream are ErrorRecord objects, not strings. Wrap with 2>&1 | ForEach-Object { "$_" }.
  • Method invocation needs parens, no spaces. $obj.Foo($a, $b), not $obj.Foo $a $b — that’s parsed as Foo returned then two strings.
  • if ($var) truthiness treats 0, empty string, $null, empty array as false; 'False' as true (it’s a non-empty string).
  • PowerShell on Linux/macOS: aliases ls/cat/mv etc. are removed so they don’t shadow native binaries — Get-ChildItem works; ls calls /bin/ls.
  • Tilde expansion bug pre-7.4 — fixed in 7.4 to expand ~ to $HOME on Windows for native commands. (7.4 release notes)
  • Heredocs (here-strings) on PowerShell 5.1: the closing "@ MUST be at column 0; even one space of indent is a parse error. (5.1 doesn’t have the indent-tolerant version.)

Citations