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.

powershell
# Looks like text — actually objects with properties
Get-Process | Get-Member -MemberType Property | Select-Object -First 5

Output:

text
   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;}
powershell
# Every property is addressable without parsing
Get-Process pwsh | Select-Object Name, Id, @{ N="MemMB"; E={ [Math]::Round($_.WorkingSet/1MB,1) } }

Output:

text
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.

powershell
# 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):

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

powershell
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):

text
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.

powershell
# 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):

text
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.

powershell
Get-Process |
  Where-Object {
    $_.CPU -gt 10 -and
    $_.WorkingSet -gt 50MB -and
    $_.Name -notmatch '^idle'
  }

Output:

text
 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.

powershell
# 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):

text
Name                Id     CPU  WorkingSet
----                --     ---  ----------
chrome            3812  487.23 313049600
Code              7720  132.09  99151872
pwsh              2948   67.44  50573312

Output (rename + compute):

text
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.

powershell
# 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):

text
   TypeName: Selected.System.ServiceProcess.ServiceController

Name        MemberType   Definition
----        ----------   ----------
Name        NoteProperty string Name=AudioEndpointBuilder

Output (-ExpandProperty form):

text
   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.

powershell
# 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 }):

text
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.

powershell
# 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 }):

text
2
4
6
8
10

Output (file size composition):

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

powershell
Get-Process | ForEach-Object `
  -Begin   { $total = 0; "Starting CPU sum…" } `
  -Process { $total += $_.CPU } `
  -End     { "Total CPU seconds: $([Math]::Round($total, 1))" }

Output:

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

powershell
# 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):

text
TotalSeconds
------------
   10.0823491

Output (parallel):

text
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.

powershell
# 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)

text
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.

powershell
# 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):

text
 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.

powershell
'10','2','1','100' | Sort-Object              # lexicographic
'10','2','1','100' | Sort-Object { [int]$_ }  # numeric

Output (lexicographic):

text
1
10
100
2

Output (numeric):

text
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.

powershell
Get-Process | Sort-Object Name -Unique | Select-Object Name | Select-Object -First 5

Output:

text
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.

powershell
# 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):

text
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.

powershell
# 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:

text
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.

powershell
# 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):

text
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.

powershell
# 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):

text
Count    : 87
Average  : 14.2731
Sum      : 1241.76
Maximum  : 487.23
Minimum  : 0
Property : CPU

Output (text stats):

text
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.

powershell
$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):

text
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.

powershell
$old = Import-Csv C:\reports\users-monday.csv
$new = Import-Csv C:\reports\users-tuesday.csv

Compare-Object $old $new -Property Email, Department

Output:

text
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.

powershell
# 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.

powershell
# 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):

text
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.

powershell
# 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):

text
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

  1. $null on 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.
  2. Piping to Format-Table then to another cmdletFormat-* emits formatting instructions, not data; the next cmdlet sees garbage. Use Select-Object for any projection that has to feed downstream code.
  3. Using += to grow an array in ForEach-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.
  4. Sorting before filteringSort-Object buffers the entire stream. Filter first with Where-Object (or -Filter) to shrink the buffer.
  5. Out-GridView on Linux/macOS — only works on Windows desktop. Install ConsoleGuiTools for a portable equivalent.
  6. -Parallel without $using: for outer variables — references to outer-scope variables silently become $null inside the parallel block. Always prefix with $using:.
  7. -ExpandProperty on an array property — emits all the array elements flat, not the array itself. If you want to keep the array, use -Property instead.

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.

powershell
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:

text
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.

powershell
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:

text
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.

powershell
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:

text
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.

powershell
$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:

text
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.

powershell
$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:

text
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.

powershell
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:

text
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; …

Sources