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 hardeningRename-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-Member on anything. Don't memorize properties — pipe to Get-Member and read.
  • Tab-completion expands. Type Get-Wi+Tab → Get-WinEvent. Type a parameter - then Tab to cycle parameters.
  • -WhatIf before destructive commands. Remove-Item -Recurse -WhatIf shows 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 -Recurse doesn'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.


Related posts