cheat sheet
PowerShell Pipelines
Deep dive on PowerShell's object pipeline: Where-Object, Select-Object, ForEach-Object, Sort-Object, Group-Object, Measure-Object, Compare-Object, Out-GridView, parallel execution, and the two filter forms.
PowerShell Pipelines — Objects, Not Text
What it is
PowerShell pipelines pass live .NET objects from one cmdlet to the next — not the line-oriented text streams that bash/zsh use. Every cmdlet on the right side of | receives whole objects with typed properties ($_.Name, $_.Length, $_.LastWriteTime), so filtering, projecting, sorting, and aggregating happen without parsing, awk field-splitting, or careful quoting. This article covers the seven pipeline cmdlets you reach for every day — Where-Object, Select-Object, ForEach-Object, Sort-Object, Group-Object, Measure-Object, Compare-Object — plus PowerShell 7's -Parallel execution and the two filter forms (script-block vs comparison-operator) that trip up newcomers.
The mental model — objects, not text
The single most important idea: a pipeline carries [System.Diagnostics.Process] objects, not lines of ps output. The first cmdlet (Get-Process, Get-ChildItem, Get-Service) emits objects one at a time; every downstream cmdlet sees $_ (alias $PSItem) bound to whatever is currently flowing through. Only at the very end of the pipeline does the formatting engine convert what's left into the table or list you see on screen.
# Looks like text — actually objects with properties
Get-Process | Get-Member -MemberType Property | Select-Object -First 5
Output:
TypeName: System.Diagnostics.Process
Name MemberType Definition
---- ---------- ----------
BasePriority Property int BasePriority {get;}
Container Property System.ComponentModel.IContainer Container {get;}
EnableRaisingEvents Property bool EnableRaisingEvents {get;set;}
ExitCode Property int ExitCode {get;}
ExitTime Property datetime ExitTime {get;}
# Every property is addressable without parsing
Get-Process pwsh | Select-Object Name, Id, @{ N="MemMB"; E={ [Math]::Round($_.WorkingSet/1MB,1) } }
Output:
Name Id MemMB
---- -- -----
pwsh 4892 142.7
pwsh 7104 98.3
The consequence: you almost never need grep, awk, cut, or sed in a PowerShell pipeline. You filter with Where-Object, project with Select-Object, and transform with ForEach-Object — all against named properties.
Where-Object — filtering the pipeline
Where-Object evaluates a condition against each incoming object and forwards only those that pass. The alias is ?. There are two filter forms: the script-block form ({ $_.Property -op value }) is fully general and handles compound conditions; the comparison-operator form (introduced in PowerShell 3) is a shorthand for single-property tests that reads more cleanly when you don't need -and / -or.
Script-block form vs comparison form
The script-block form runs your code for every object — you have access to the full object via $_ and can chain operators. The comparison form takes a property name and an operator/value and is internally rewritten to the equivalent script block, but it's terser for the 80% case.
# Script-block form
Get-Process | Where-Object { $_.CPU -gt 100 }
Get-Process | Where-Object { $_.Name -like 'node*' -and $_.WorkingSet -gt 100MB }
# Comparison form (PowerShell 3+)
Get-Process | Where-Object CPU -gt 100
Get-Process | Where-Object Name -like 'node*'
# Alias: ? or where
Get-Process | ? CPU -gt 100
Get-Process | where { $_.WorkingSet -gt 50MB }
Output (comparison form on a quiet machine):
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
52 312.44 298.17 487.23 3812 1 chrome
38 201.06 188.40 215.66 5104 1 pwsh
21 88.32 94.71 132.09 7720 1 Code
Use the comparison form when you have one property and one operator. Switch to the script block the moment you need a second condition, a calculated value, or a method call.
All comparison operators work in Where-Object
Any comparison operator is accepted — equality (-eq/-ne), ordering (-lt/-gt/-le/-ge), pattern (-like/-notlike/-match/-notmatch), set (-in/-notin/-contains/-notcontains), and type (-is/-isnot). The c prefix (-ceq, -cmatch) flips to case-sensitive; the i prefix (-ieq) is explicit case-insensitive (the default).
Get-Process | Where-Object Name -eq 'svchost'
Get-Process | Where-Object Name -match '^sql'
Get-Process | Where-Object Name -in @('chrome','firefox','msedge')
Get-Process | Where-Object Name -notlike 'idle*'
Get-Service | Where-Object Status -eq 'Running'
Get-ChildItem | Where-Object LastWriteTime -gt (Get-Date).AddDays(-7)
Output (Get-Service | Where-Object Status -eq 'Running' | Select-Object -First 5):
Status Name DisplayName
------ ---- -----------
Running AudioEndpointBu...Windows Audio Endpoint Builder
Running Audiosrv Windows Audio
Running BFE Base Filtering Engine
Running BITS Background Intelligent Transf...
Running CDPSvc Connected Devices Platform Se...
Null and truthy checks
A common bug: Where-Object { $_.Company -ne $null } fails to filter because $null should be on the left in PowerShell comparisons. Always write $null -ne $_.X. Even cleaner — rely on PowerShell's truthiness rules: a bare Where-Object Company excludes anything where Company is $null, empty string, zero, or an empty array.
# Idiomatic null check — $null on the left
Get-Process | Where-Object { $null -ne $_.Company }
# Truthy form — equivalent for most cases, terser
Get-Process | Where-Object Company
# Filter to only items with a window title
Get-Process | Where-Object MainWindowTitle
Output (Get-Process | Where-Object MainWindowTitle | Select-Object Name, MainWindowTitle -First 3):
Name MainWindowTitle
---- ---------------
Code cheatsheets - Visual Studio Code
explorer File Explorer
pwsh Administrator: PowerShell 7
Compound conditions
When you need more than one test, use the script-block form with -and, -or, -not. Line-continue with backticks (or just leave the open brace on its own line) for readability.
Get-Process |
Where-Object {
$_.CPU -gt 10 -and
$_.WorkingSet -gt 50MB -and
$_.Name -notmatch '^idle'
}
Output:
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
52 312.44 298.17 487.23 3812 1 chrome
21 88.32 94.71 132.09 7720 1 Code
14 45.11 48.22 67.44 2948 1 pwsh
Select-Object — projecting and limiting
Select-Object picks specific properties, adds calculated columns, and limits how many items pass downstream. It is PowerShell's SQL SELECT — -Property picks columns, -First/-Last/-Skip apply LIMIT/OFFSET, and calculated-property hashtables (@{ N='alias'; E={ expression } }) act like AS clauses. Unlike Format-*, the output of Select-Object is still objects, so the pipeline can keep going.
Picking and renaming columns
The simplest use is to pass property names you care about — anything else is dropped. To rename or compute, use a hashtable with Name/N for the new column name and Expression/E for the script block that computes the value.
# Pick named properties
Get-Process | Select-Object Name, Id, CPU, WorkingSet
# Rename and compute
Get-Process |
Select-Object Name,
@{ N='MemMB'; E={ [Math]::Round($_.WorkingSet/1MB,1) } },
@{ N='CPU%'; E={ '{0:N1}' -f $_.CPU } }
# All properties + a calculated one
Get-Process | Select-Object *, @{ N='MemGB'; E={ $_.WorkingSet/1GB } }
# Exclude noisy auto-properties
Get-Process | Select-Object * -ExcludeProperty __*
Output (Select-Object Name, Id, CPU, WorkingSet):
Name Id CPU WorkingSet
---- -- --- ----------
chrome 3812 487.23 313049600
Code 7720 132.09 99151872
pwsh 2948 67.44 50573312
Output (rename + compute):
Name MemMB CPU%
---- ----- ----
chrome 298.5 487.2
Code 94.6 132.1
pwsh 48.2 67.4
-ExpandProperty — unwrap to raw values
-ExpandProperty returns the value of a single property as a flat list, not as a wrapped object. This is the right way to get an array of strings (or any scalar) ready to pass into another tool — (Get-Service).Name is the dot-access equivalent.
# Without -ExpandProperty: objects with a single Name property
Get-Service | Select-Object -Property Name | Get-Member
# With -ExpandProperty: raw strings
Get-Service | Select-Object -ExpandProperty Name | Get-Member
Output (-Property form):
TypeName: Selected.System.ServiceProcess.ServiceController
Name MemberType Definition
---- ---------- ----------
Name NoteProperty string Name=AudioEndpointBuilder
Output (-ExpandProperty form):
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property int Length {get;}
-First, -Last, -Skip, -Unique
These are the slicing options — they trim the stream before the result hits the formatter or the next cmdlet. -Unique is property-aware: Select-Object -Unique Name returns the first object seen for each unique Name value, but on a raw pipeline of values it deduplicates the values themselves.
# Top 10 CPU consumers
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
# Tail of a sorted list
Get-Process | Sort-Object WorkingSet | Select-Object -Last 5
# Skip the first 5 (pagination)
Get-Process | Sort-Object Name | Select-Object -Skip 5 -First 10
# Unique by Name
Get-Process | Select-Object -Unique Name
# SkipUntil / SkipWhile (PowerShell 6+)
1..10 | Select-Object -SkipWhile { $_ -lt 5 } # → 5..10
1..10 | Select-Object -SkipUntil { $_ -gt 7 } # → 8..10
Output (Select-Object -SkipWhile { $_ -lt 5 }):
5
6
7
8
9
10
ForEach-Object — transforming each item
ForEach-Object runs a script block once per pipeline item, with $_ bound to the current object — the pipeline map step. It supports -Begin / -Process / -End blocks for one-time setup and teardown, and in PowerShell 7+ adds -Parallel for concurrent execution. The alias is %.
Basic transformation
Whatever the block emits becomes the next pipeline item. Use it when you need to compute a derived value per item, side-effect (start/stop services), or build composite output strings.
# Double each number
1..5 | ForEach-Object { $_ * 2 }
# Compose a string per file
Get-ChildItem *.log | ForEach-Object {
$kb = [Math]::Round($_.Length/1KB, 1)
"$($_.Name): $kb KB"
}
# Side effect — start every stopped service whose name matches
Get-Service | Where-Object Status -eq 'Stopped' |
ForEach-Object { Start-Service $_.Name -WhatIf }
Output (1..5 | ForEach-Object { $_ * 2 }):
2
4
6
8
10
Output (file size composition):
app.log: 142.3 KB
error.log: 38.7 KB
access.log: 987.1 KB
Begin / Process / End blocks
For accumulators that need initialization, use -Begin (runs once before the first item), -Process (runs per item — the default block goes here), and -End (runs once after the last item).
Get-Process | ForEach-Object `
-Begin { $total = 0; "Starting CPU sum…" } `
-Process { $total += $_.CPU } `
-End { "Total CPU seconds: $([Math]::Round($total, 1))" }
Output:
Starting CPU sum…
Total CPU seconds: 1241.8
-Parallel (PowerShell 7+)
-Parallel runs the script block on multiple runspaces concurrently. Use -ThrottleLimit to cap the number of concurrent threads (default: 5). To reference an outer-scope variable, prefix it with $using: — the parallel runspace gets a snapshot, not a live reference, so mutations don't propagate back. -Parallel graduated through several experimental refinements and remains stable in PowerShell 7.6 LTS (March 2026).
# Sequential — ~10 seconds
Measure-Command {
1..10 | ForEach-Object { Start-Sleep -Seconds 1; "done $_" }
} | Select-Object TotalSeconds
# Parallel — ~2 seconds with throttle 5
Measure-Command {
1..10 | ForEach-Object -Parallel { Start-Sleep -Seconds 1; "done $_" } -ThrottleLimit 5
} | Select-Object TotalSeconds
# Pass an outer variable in with $using:
$multiplier = 3
1..5 | ForEach-Object -Parallel { $_ * $using:multiplier }
Output (sequential):
TotalSeconds
------------
10.0823491
Output (parallel):
TotalSeconds
------------
2.1247163
-Parallel has overhead — for purely CPU-bound work over a small list, the runspace setup time can dominate. It shines for I/O-bound tasks: HTTP requests, remote queries, file uploads.
foreach statement vs ForEach-Object
The foreach statement (no hyphen) iterates an in-memory collection without pipeline plumbing — it is significantly faster on large arrays because it doesn't create a runspace for each item. Use it inside scripts when you already have all the data in memory.
# Statement form — fastest for in-memory data
foreach ($p in (Get-Process)) {
if ($p.CPU -gt 100) { $p.Name }
}
# Method form — even faster, no pipeline at all
(Get-Process).ForEach({ if ($_.CPU -gt 100) { $_.Name } })
# Cmdlet form — flexible, streams from upstream
Get-Process | ForEach-Object { if ($_.CPU -gt 100) { $_.Name } }
Output: (each form returns the same set of names)
chrome
Code
pwsh
Sort-Object — ordering the stream
Sort-Object orders pipeline objects by one or more property values, ascending by default. Add -Descending to reverse, or use the hashtable form (@{ E='Name'; Asc=$true }) to mix sort directions across multiple keys. Sort-Object is a blocking cmdlet — it has to buffer the entire stream before emitting the first item.
Single and multi-key sorts
Pass one or more property names; pass a script block to sort on a calculated value. Sorts are stable, so equal keys preserve input order.
# Single property
Get-Process | Sort-Object CPU
Get-Process | Sort-Object CPU -Descending
# Multi-key — primary then tiebreaker
Get-Process | Sort-Object Name, CPU -Descending
# Mixed direction per key
Get-Process | Sort-Object @{ E='Name'; Asc=$true }, @{ E='CPU'; Desc=$true }
# Sort by calculated expression
Get-ChildItem | Sort-Object { $_.LastWriteTime }
Get-ChildItem | Sort-Object { $_.Length / $_.Name.Length } # bytes per char
Output (Get-Process | Sort-Object CPU -Descending | Select -First 5):
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
52 312.44 298.17 487.23 3812 1 chrome
38 201.06 188.40 215.66 5104 1 pwsh
21 88.32 94.71 132.09 7720 1 Code
14 45.11 48.22 67.44 2948 1 pwsh
9 22.08 19.88 31.12 6372 1 SearchIndexer
Sorting strings as numbers
Sorting '10','2','1' lexicographically yields 1, 10, 2. Pass a script block that casts to [int] (or any other type) to coerce the comparison.
'10','2','1','100' | Sort-Object # lexicographic
'10','2','1','100' | Sort-Object { [int]$_ } # numeric
Output (lexicographic):
1
10
100
2
Output (numeric):
1
2
10
100
-Unique with Sort-Object
Sort-Object -Unique deduplicates while sorting — handy when the upstream stream may have duplicates and you want one pass for both. For property-aware uniqueness, sort first, then Select-Object -Unique.
Get-Process | Sort-Object Name -Unique | Select-Object Name | Select-Object -First 5
Output:
Name
----
ApplicationFrameHost
chrome
Code
conhost
csrss
Group-Object — bucketing by a key
Group-Object collects pipeline objects into groups keyed by a shared property value. It returns one GroupInfo object per distinct key with Name (the key), Count, and Group (the matching objects). It's the pipeline equivalent of SQL GROUP BY. Use -NoElement to drop the Group member and only keep counts — useful on large collections.
# Count processes by name
Get-Process | Group-Object Name -NoElement | Sort-Object Count -Descending
# Group files by extension
Get-ChildItem -Recurse -File | Group-Object Extension | Sort-Object Count -Descending
Output (Get-Process | Group-Object Name -NoElement | Sort-Object Count -Descending | Select-Object -First 5):
Count Name
----- ----
18 svchost
8 chrome
4 RuntimeBroker
3 conhost
2 pwsh
Group then aggregate
The common pattern is Group-Object followed by ForEach-Object that extracts a per-group summary — total bytes, average CPU, top item, etc.
# Total disk per file extension
Get-ChildItem C:\logs -Recurse -File |
Group-Object Extension |
ForEach-Object {
[pscustomobject]@{
Ext = $_.Name
Files = $_.Count
TotalMB = [Math]::Round(($_.Group | Measure-Object Length -Sum).Sum / 1MB, 2)
}
} |
Sort-Object TotalMB -Descending
Output:
Ext Files TotalMB
--- ----- -------
.log 12 142.55
.txt 4 38.71
.json 3 9.42
.csv 1 0.18
Bucketing with a calculated key
Pass a script block to Group-Object to bucket on something derived — date, size band, log level. The block's return value becomes the group key.
# Files modified per day
Get-ChildItem C:\logs -Recurse -File |
Group-Object { $_.LastWriteTime.Date } |
Sort-Object Name
# Bucket processes into 100 MB working-set bands
Get-Process |
Group-Object { [Math]::Floor($_.WorkingSet / 100MB) * 100 } -NoElement |
Sort-Object { [int]$_.Name }
Output (per-day):
Count Name Group
----- ---- -----
4 5/22/2026 12:00:00 AM {app.log, error.log, …}
3 5/23/2026 12:00:00 AM {access.log, debug.log, …}
5 5/24/2026 12:00:00 AM {server.log, audit.log, …}
Measure-Object — count, sum, average, min, max
Measure-Object aggregates numeric values from a pipeline — -Sum, -Average, -Minimum, -Maximum, and -StandardDeviation (PowerShell 6+). With text input it can also count -Word, -Line, and -Character. It is wc + basic stats, but composable.
# Count any pipeline
Get-Process | Measure-Object
# Sum / average / min / max on a numeric property
Get-Process | Measure-Object CPU -Sum -Average -Minimum -Maximum
# Aggregate a derived property — pipe through Select first
Get-Process |
Select-Object @{ N='MemMB'; E={ $_.WorkingSet/1MB } } |
Measure-Object MemMB -Sum -Average
# Text stats — words, lines, chars
Get-Content C:\Users\Alice\notes.txt | Measure-Object -Word -Line -Character
Output (Measure-Object CPU -Sum -Average -Minimum -Maximum):
Count : 87
Average : 14.2731
Sum : 1241.76
Maximum : 487.23
Minimum : 0
Property : CPU
Output (text stats):
Lines Words Characters Property
----- ----- ---------- --------
342 2891 18423
Compare-Object — diffing two sets
Compare-Object diffs two collections — the reference set (left) and the difference set (right) — and tags each unique item with a SideIndicator: <= for ref-only, => for diff-only. Use -IncludeEqual to also emit matching items, or -PassThru to forward the raw objects (annotated with SideIndicator as a NoteProperty) for further pipeline work.
$before = Get-Service | Select-Object -ExpandProperty Name
# … install something …
$after = Get-Service | Select-Object -ExpandProperty Name
# What changed?
Compare-Object $before $after
# Only services that were added
Compare-Object $before $after | Where-Object SideIndicator -eq '=>'
# Compare files line-by-line
Compare-Object (Get-Content old.txt) (Get-Content new.txt)
Output (after installing two new services):
InputObject SideIndicator
----------- -------------
WSearchIndexer =>
SQLBrowser =>
OldLegacySvc <=
Compare on a property — -Property
By default Compare-Object uses .ToString() for each item, which is fine for strings but misses changes in object properties. Pass -Property to compare on one or more named properties — useful for diffing rows of data, registry exports, or csv imports.
$old = Import-Csv C:\reports\users-monday.csv
$new = Import-Csv C:\reports\users-tuesday.csv
Compare-Object $old $new -Property Email, Department
Output:
Email Department SideIndicator
----- ---------- -------------
alice@example.com Engineering =>
alice@example.com Sales <=
bob@example.com Engineering =>
That output reads as: alice@example.com moved from Sales to Engineering, bob@example.com is new in Engineering.
Out-GridView — interactive grid
Out-GridView opens a sortable, filterable, click-to-select grid window for the pipeline content. With -PassThru, selected rows are sent back through the pipeline when you close the window — turning the grid into an interactive picker. It only works on Windows desktop (Windows PowerShell or PowerShell 7 with GUI); on Linux/macOS it errors out.
# Interactive view of running services
Get-Service | Out-GridView -Title 'Services'
# Pick services interactively, then act on them
$picked = Get-Service | Where-Object Status -eq 'Running' |
Out-GridView -OutputMode Multiple -Title 'Choose services to restart'
$picked | ForEach-Object { Restart-Service $_.Name -WhatIf }
Output: (GUI window — no console output. With -PassThru/-OutputMode, selected rows return to the pipeline.)
For cross-platform interactive selection, install Microsoft.PowerShell.ConsoleGuiTools (Install-Module ConsoleGuiTools) and use Out-ConsoleGridView — a terminal-based equivalent that runs anywhere.
Tee-Object — split the pipeline
Tee-Object is a T-junction: it forwards every object downstream unchanged and captures a copy into a variable or file. Use it for inspection mid-pipeline, or to write a log of intermediate state without breaking the chain.
# Save the full process list to a variable AND continue measuring
Get-Process |
Tee-Object -Variable procs |
Where-Object CPU -gt 10 |
Measure-Object CPU -Sum
$procs.Count # how many objects we saw before the Where filter
# Log to a file mid-pipeline
Get-ChildItem C:\logs -Recurse |
Tee-Object -FilePath C:\Users\Alice\seen.log -Append |
Where-Object Length -gt 10MB
Output (Measure-Object result):
Count : 87
Average :
Sum : 1241.76
Maximum :
Minimum :
Property : CPU
The two filter forms — when to pick which
PowerShell has two idiomatic ways to filter, and the choice changes performance: Where-Object (or ?) sits at the end of the pipeline, while -Filter is a parameter on the upstream cmdlet that pushes the filter into the provider — meaning the cmdlet itself never produces the objects that get rejected. For large file trees, registry traversals, or AD queries, -Filter is dramatically faster.
# Slow: Get-ChildItem yields every file, then Where-Object discards 99%
Get-ChildItem C:\Windows -Recurse | Where-Object Name -like '*.dll'
# Fast: -Filter pushes the wildcard into the FileSystem provider
Get-ChildItem C:\Windows -Recurse -Filter '*.dll'
# AD: -Filter is mandatory; Where-Object yanks every user object across the wire
Get-ADUser -Filter 'Enabled -eq $true -and Department -eq "Engineering"'
Output (timing comparison, 200k files):
Where-Object form: TotalSeconds : 47.18
-Filter form: TotalSeconds : 6.42
Rule of thumb: try -Filter first; fall back to Where-Object only when the upstream cmdlet doesn't accept the predicate you need.
Common pitfalls
$nullon the right of a comparison — Always write$null -ne $_.Property, not$_.Property -ne $null. The second form short-circuits when the property is itself a collection, producing surprising results.- Piping to
Format-Tablethen to another cmdlet —Format-*emits formatting instructions, not data; the next cmdlet sees garbage. UseSelect-Objectfor any projection that has to feed downstream code. - Using
+=to grow an array inForEach-Object— Every+=reallocates the whole array. Either collect with[System.Collections.Generic.List[object]]::new()or just let the pipeline collect the output of the block. - Sorting before filtering —
Sort-Objectbuffers the entire stream. Filter first withWhere-Object(or-Filter) to shrink the buffer. Out-GridViewon Linux/macOS — only works on Windows desktop. InstallConsoleGuiToolsfor a portable equivalent.-Parallelwithout$using:for outer variables — references to outer-scope variables silently become$nullinside the parallel block. Always prefix with$using:.-ExpandPropertyon an array property — emits all the array elements flat, not the array itself. If you want to keep the array, use-Propertyinstead.
Real-world recipes
Top 10 largest log files modified this week
A common diagnostic: scan a logs tree, keep only files modified in the last seven days, sort by size, and print the ten biggest with human-readable sizes.
Get-ChildItem C:\Users\Alice\logs -Recurse -File -Filter '*.log' |
Where-Object LastWriteTime -gt (Get-Date).AddDays(-7) |
Sort-Object Length -Descending |
Select-Object -First 10 |
Select-Object FullName,
@{ N='SizeMB'; E={ [Math]::Round($_.Length / 1MB, 2) } },
@{ N='ModifiedAgoDays'; E={ [Math]::Round(((Get-Date) - $_.LastWriteTime).TotalDays, 1) } } |
Format-Table -AutoSize
Output:
FullName SizeMB ModifiedAgoDays
-------- ------ ---------------
C:\Users\Alice\logs\app\application.log 487.21 0.2
C:\Users\Alice\logs\nginx\access.log 312.04 1.4
C:\Users\Alice\logs\app\error.log 142.55 0.5
C:\Users\Alice\logs\sql\trace.log 98.13 3.1
C:\Users\Alice\logs\sys\eventlog.log 87.42 2.0
C:\Users\Alice\logs\app\debug.log 74.55 6.8
C:\Users\Alice\logs\nginx\error.log 38.71 1.4
C:\Users\Alice\logs\app\audit.log 12.04 4.2
C:\Users\Alice\logs\app\startup.log 9.42 0.0
C:\Users\Alice\logs\sys\update.log 6.18 5.5
Total disk usage per file type, top to bottom
Group every file under a tree by extension, sum the bytes per group, and print the result sorted by total size.
Get-ChildItem C:\Users\Alice\Projects -Recurse -File -ErrorAction SilentlyContinue |
Group-Object Extension |
ForEach-Object {
[pscustomobject]@{
Extension = if ($_.Name) { $_.Name } else { '(none)' }
Files = $_.Count
TotalMB = [Math]::Round(($_.Group | Measure-Object Length -Sum).Sum / 1MB, 2)
}
} |
Sort-Object TotalMB -Descending |
Select-Object -First 10
Output:
Extension Files TotalMB
--------- ----- -------
.mp4 42 4218.55
.dll 1283 872.14
.js 918 142.71
.ts 412 88.42
.png 202 64.18
.exe 18 54.21
.zip 12 42.88
.pdf 28 31.07
.json 304 18.14
.md 152 8.42
Aggregate event log errors by source
Pull the last day of System-log Error events, group by source, count each group, and surface the noisiest providers.
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2 # 2 = Error
StartTime = (Get-Date).AddDays(-1)
} |
Group-Object ProviderName -NoElement |
Sort-Object Count -Descending |
Select-Object -First 10
Output:
Count Name
----- ----
42 DCOM
18 Service Control Manager
12 disk
8 Schannel
4 Microsoft-Windows-Kernel-PnP
Parallel HTTPS health-check over a host list
Hit a small fleet in parallel and aggregate the status code, response time, and final URL.
$hosts = 'web01','web02','web03','web04','web05'
$hosts | ForEach-Object -Parallel {
$start = Get-Date
try {
$r = Invoke-WebRequest -Uri "https://$_/healthz" -TimeoutSec 5 -SkipHttpErrorCheck
[pscustomobject]@{
Host = $_
Status = $r.StatusCode
DurationMs = [Math]::Round(((Get-Date) - $start).TotalMilliseconds, 0)
Final = $r.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
}
} catch {
[pscustomobject]@{
Host = $_; Status = 'ERR'; DurationMs = -1; Final = $_.Exception.Message
}
}
} -ThrottleLimit 8 |
Sort-Object Status, DurationMs
Output:
Host Status DurationMs Final
---- ------ ---------- -----
web02 200 87 https://web02/healthz
web04 200 91 https://web04/healthz
web01 200 118 https://web01/healthz
web05 200 134 https://web05/healthz
web03 503 612 https://web03/healthz
Diff two CSV exports — what changed between yesterday and today?
Import two CSVs, compare by primary key and a payload column, and surface adds, removes, and field flips.
$y = Import-Csv C:\Users\Alice\snapshots\users-yesterday.csv
$t = Import-Csv C:\Users\Alice\snapshots\users-today.csv
Compare-Object $y $t -Property Email, Status, Department -PassThru |
Sort-Object Email, SideIndicator |
Select-Object Email, Status, Department, SideIndicator |
Format-Table -AutoSize
Output:
Email Status Department SideIndicator
----- ------ ---------- -------------
alice@example.com Active Engineering =>
alice@example.com Active Sales <=
bob@example.com Disabled Engineering =>
carla@example.com Active Marketing =>
Reads as: Alice moved from Sales to Engineering, Bob was disabled, Carla was added.
Find duplicate files by content hash
Hash every file under a tree, group by hash, and keep groups that have more than one member — those are duplicates.
Get-ChildItem C:\Users\Alice\Downloads -Recurse -File |
ForEach-Object {
[pscustomobject]@{
Path = $_.FullName
Hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
Size = $_.Length
}
} |
Group-Object Hash |
Where-Object Count -gt 1 |
ForEach-Object {
[pscustomobject]@{
Hash = $_.Name
Copies = $_.Count
SizeKB = [Math]::Round(($_.Group[0].Size) / 1KB, 1)
WastedKB = [Math]::Round(($_.Group[0].Size * ($_.Count - 1)) / 1KB, 1)
Paths = ($_.Group.Path -join '; ')
}
} |
Sort-Object WastedKB -Descending
Output:
Hash Copies SizeKB WastedKB Paths
---- ------ ------ -------- -----
3A7BD3E2… 4 8421 25263.0 C:\Users\Alice\Downloads\installer.exe; …
F92C18AB… 2 480 480.0 C:\Users\Alice\Downloads\report.pdf; …