I script PowerShell daily, mostly on Windows Server boxes. This is the snippets I keep typing, organized so I can find them when I'm tired.
Not a tutorial. Reference. If you've never written PowerShell, start with How I use Claude to write PowerShell scripts — that post covers the workflow, this one is the language.
Pipeline patterns
PowerShell pipes objects, not text. The patterns:
# Filter
Get-Service | Where-Object { $_.Status -eq 'Stopped' }
# Project (select fields)
Get-Process | Select-Object Name, CPU, WorkingSet -First 10
# Sort
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5
# Group
Get-ChildItem -Recurse | Group-Object Extension | Sort-Object Count -Descending
# ForEach
Get-ChildItem *.log | ForEach-Object { Compress-Archive $_ "$($_.Name).zip" }
Where-Object and ForEach-Object have aliases ? and %. Don't use them in scripts (unreadable), do use them at the prompt.
Service management
# State
Get-Service -Name spooler
Get-Service | Where-Object Status -eq Running
# Lifecycle
Start-Service spooler
Stop-Service spooler -Force
Restart-Service spooler
# Startup type
Set-Service spooler -StartupType Disabled
Set-Service spooler -StartupType Automatic
For services that don't appear in Get-Service (or to manage cross-machine), sc.exe and Get-CimInstance Win32_Service are the alternatives.
Files and paths
# Find recent
Get-ChildItem C:\Logs -Recurse -Filter *.log |
Where-Object LastWriteTime -gt (Get-Date).AddDays(-7)
# Big files
Get-ChildItem C:\ -Recurse -ErrorAction SilentlyContinue |
Sort-Object Length -Descending |
Select-Object FullName, Length -First 20
# Delete old files
Get-ChildItem C:\Backup -Filter *.bak |
Where-Object LastWriteTime -lt (Get-Date).AddDays(-30) |
Remove-Item -Force
-ErrorAction SilentlyContinue swallows access-denied noise on system folders. Don't use it elsewhere — it hides real errors.
Event logs
# Recent failed logons
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625} -MaxEvents 20
# Unexpected shutdowns
Get-WinEvent -FilterHashtable @{LogName='System'; ID=6008} -MaxEvents 5
# Errors in last hour, all logs
Get-WinEvent -FilterHashtable @{
LogName='Application','System'
Level=1,2,3
StartTime=(Get-Date).AddHours(-1)
}
-FilterHashtable is fast (server-side filter). Get-WinEvent | Where-Object is slow (all events come back, then filter). Always filter at source for big logs.
Useful for hunting after RDP brute-force attempts or auditing post-incident.
Networking
# Listening ports
Get-NetTCPConnection -State Listen | Select LocalAddress, LocalPort, OwningProcess
# Process owning a port
Get-NetTCPConnection -LocalPort 443 |
ForEach-Object { Get-Process -Id $_.OwningProcess }
# Test connectivity (replaces ping + telnet for ports)
Test-NetConnection google.com -Port 443
Test-Connection 8.8.8.8 -Count 4
# DNS lookup
Resolve-DnsName example.com -Type A
Resolve-DnsName example.com -Server 1.1.1.1
Test-NetConnection is slow. For quick port checks, tnc -ComputerName x -Port y -InformationLevel Quiet returns just True/False.
Remoting
# One-shot command
Invoke-Command -ComputerName server01 -ScriptBlock { Get-Service spooler }
# Persistent session
$s = New-PSSession -ComputerName server01
Invoke-Command -Session $s -ScriptBlock { ... }
Enter-PSSession $s # interactive
Remove-PSSession $s
# Multiple servers, parallel
Invoke-Command -ComputerName srv01, srv02, srv03 -ScriptBlock {
Get-Service | Where-Object Status -eq Stopped
}
WinRM has to be enabled on the target (Enable-PSRemoting -Force). Cross-domain or workgroup needs TrustedHosts set on the client.
Scheduled tasks
# List
Get-ScheduledTask | Where-Object State -ne Disabled
# Create one
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-File C:\Scripts\backup.ps1'
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
Register-ScheduledTask -TaskName 'Nightly Backup' `
-Action $action -Trigger $trigger -RunLevel Highest -User SYSTEM
# Run now
Start-ScheduledTask -TaskName 'Nightly Backup'
# Delete
Unregister-ScheduledTask -TaskName 'Nightly Backup' -Confirm:$false
Scheduled tasks are the cron equivalent. SYSTEM-account tasks don't have a desktop or network drives — UNC paths and explicit credentials only.
User and account management
# Local users
Get-LocalUser
New-LocalUser -Name svc-app -NoPassword -AccountNeverExpires
Set-LocalUser -Name svc-app -Password (ConvertTo-SecureString 'X' -AsPlainText -Force)
Add-LocalGroupMember -Group Administrators -Member svc-app
# AD users (RSAT installed)
Get-ADUser -Filter "Enabled -eq 'true'" -Properties LastLogonDate
New-ADUser -Name "Jane Doe" -SamAccountName jdoe -Enabled $true `
-AccountPassword (Read-Host -AsSecureString)
Built-in Administrator rename pattern from RDP hardening — Rename-LocalUser.
For creating Windows users from cmd, net user still works and is shorter for one-off creates.
Error handling
try {
Get-Item C:\does-not-exist -ErrorAction Stop
} catch {
Write-Error "File not found: $_"
} finally {
# cleanup
}
-ErrorAction Stop is mandatory for try/catch to fire on cmdlet errors. Without it, cmdlet errors are non-terminating and skip the catch block.
For scripts:
$ErrorActionPreference = 'Stop' # at top of script
Set-StrictMode -Version Latest # surfaces typos as errors
These two lines turn PowerShell from forgiving to actually-tells-you-when-it-broke. Use them in every script.
Useful one-liners
# Disk usage by folder
Get-ChildItem C:\Users -Directory | ForEach-Object {
$size = (Get-ChildItem $_.FullName -Recurse -ErrorAction SilentlyContinue |
Measure-Object Length -Sum).Sum / 1GB
[PSCustomObject]@{ Folder=$_.Name; SizeGB=[math]::Round($size,2) }
} | Sort-Object SizeGB -Descending
# Top 10 processes by RAM
Get-Process | Sort-Object WorkingSet -Descending |
Select Name, @{n='RAM_MB';e={[math]::Round($_.WorkingSet/1MB,1)}} -First 10
# Last 5 reboots
Get-WinEvent -FilterHashtable @{LogName='System'; ID=6005} -MaxEvents 5 |
Select TimeCreated, Message
# WHOIS-equivalent for IPs (uses RIPE)
Invoke-RestMethod "https://stat.ripe.net/data/whois/data.json?resource=8.8.8.8"
Tips that took me too long to learn
Get-Memberon anything. Don't memorize properties — pipe toGet-Memberand read.- Tab-completion expands. Type
Get-Wi+Tab →Get-WinEvent. Type a parameter-then Tab to cycle parameters. -WhatIfbefore destructive commands.Remove-Item -Recurse -WhatIfshows what would happen. Add it, read output, remove it, run for real.- PowerShell 7 ≠ Windows PowerShell 5.1. New cmdlets work on 7, not 5.1. On Server, you usually have 5.1 — install 7 explicitly if you need it (
winget install Microsoft.PowerShell). - The pipeline streams.
Get-ChildItem -Recursedoesn't materialize everything before the next stage runs. Filter early to keep memory low.
When scripting gets bigger
Scripts longer than ~50 lines benefit from being a module (.psm1) with discrete functions. Each function does one thing, has comment-based help, gets unit-tested with Pester. The transition is the same one as bash → Python — pragmatic, not theoretical.
For longer projects I lean on Claude — see the pattern in the workflow post. The cheatsheet here is the layer underneath: knowing the language so you can read and verify what gets generated.