Filtering Early: Reduce Noise with Where-Object
Most cmdlets return more data than you need. The fastest way to turn results into usable information is to filter as early as possible in the pipeline. In PowerShell, filtering is typically done with Where-Object, which keeps only the objects that match a condition.
How Where-Object Thinks
Where-Object evaluates each incoming object and returns it only if the script block is $true. Inside the script block, $_ represents “the current object.”
Get-Service | Where-Object { $_.Status -eq 'Running' }Common comparison operators you will use:
-eqequals,-nenot equals-gtgreater than,-ltless than-gegreater or equal,-leless or equal-likewildcard match (use*)-matchregex match
Task: List Services by Status (Step-by-step)
Goal: quickly see only stopped services, then only running services.
Get all services:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Get-ServiceFilter to stopped services:
Get-Service | Where-Object { $_.Status -eq 'Stopped' }Filter to running services:
Get-Service | Where-Object { $_.Status -eq 'Running' }
Filtering Tips for Readability and Reproducibility
Keep conditions explicit. Prefer
$_.Status -eq 'Running'over relying on truthy/falsey behavior.Filter before sorting/formatting. It’s easier to reason about and usually faster.
Use parentheses for clarity when combining conditions:
Get-Service | Where-Object { $_.Status -eq 'Running' -and $_.Name -like 'Win*' }
Shaping Output: Choose and Create Properties with Select-Object
After filtering, the next step is shaping: selecting only the properties you need and optionally creating new ones. This is done with Select-Object. Shaping makes output easier to read, easier to export, and more stable over time (reproducible) because you control exactly what fields exist.
Select Only the Properties You Need
Get-Service | Select-Object Name, Status, StartTypeYou can also limit the number of results:
Get-Process | Select-Object -First 10Calculated Properties (Custom Columns)
Calculated properties let you add a property that doesn’t exist on the object (or transform an existing one). The most common pattern is a hashtable with Name (or Label) and Expression.
Get-Process | Select-Object Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }}This creates a new property called CPUSeconds derived from the original CPU property.
Task: Identify Top CPU Processes (Step-by-step)
Goal: show the top processes by total CPU time, with a readable CPU column.
Start with processes (unfiltered):
Get-ProcessSort by CPU descending (highest first):
Get-Process | Sort-Object CPU -DescendingTake the top 10 and select useful properties:
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, CPUShape the CPU value into a rounded column:
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }}
Note: CPU is total processor time used since the process started (not “current CPU %”). It’s still very useful for identifying heavy consumers over time.
Task: Disk Usage Summary (Step-by-step)
Goal: show drive size, free space, and percent free in a compact summary.
A common source for disk information is Get-PSDrive (for file system drives). It returns size and free space in bytes.
List file system drives:
Get-PSDrive -PSProvider FileSystemSelect and calculate readable sizes (GB) and percent free:
Get-PSDrive -PSProvider FileSystem | Select-Object Name, @{Name='SizeGB';Expression={ [math]::Round(($_.Used + $_.Free) / 1GB, 2) }}, @{Name='FreeGB';Expression={ [math]::Round($_.Free / 1GB, 2) }}, @{Name='PercentFree';Expression={ if (($_.Used + $_.Free) -gt 0) { [math]::Round(($_.Free / ($_.Used + $_.Free)) * 100, 1) } else { 0 } }}
This is a good example of shaping output for clarity: you convert raw bytes into human-friendly units and add a percentage that’s easy to scan.
Presenting Output: Format-Table and Format-List (Formatting Is a Final Step)
Formatting cmdlets control how results are displayed on screen. The key rule is: formatting should be the final step when your goal is human-readable output. If you format too early, you often turn rich objects into formatting instructions, which breaks sorting, filtering, exporting, and further processing.
Why Formatting Should Be Last
Compare these two approaches:
# Good: shape first, then format for display at the end (still reproducible and exportable before formatting if needed) Get-Service | Where-Object { $_.Status -eq 'Running' } | Select-Object Name, Status, StartType | Format-Table -AutoSize# Risky: formatting too early limits what you can do afterward Get-Service | Format-Table Name, Status | Where-Object { $_.Status -eq 'Running' }The second example is problematic because after Format-Table, you no longer have the original service objects in a usable form for filtering and exporting.
Use Format-Table for Scan-Friendly Rows
Tables are best when you want to compare items across the same columns.
Get-Service | Where-Object { $_.Status -eq 'Stopped' } | Select-Object Name, Status, StartType | Format-Table -AutoSizeHelpful table options:
-AutoSizeadjusts column widths (best for small outputs)-Wrapwraps long text instead of truncating
Use Format-List for Detail Views
Lists are best when you want to inspect one item at a time or see many properties without horizontal truncation.
Get-Process | Sort-Object CPU -Descending | Select-Object -First 3 | Format-List *Or list specific properties:
Get-Process | Sort-Object CPU -Descending | Select-Object -First 3 Name, Id, CPU, StartTime | Format-ListPattern to Remember: Filter → Shape → Present
| Stage | Cmdlets | Purpose |
|---|---|---|
| Filter | Where-Object | Keep only what matters |
| Shape | Select-Object (incl. calculated properties) | Choose fields, create readable values |
| Present | Format-Table, Format-List | Display for humans (final step) |
Exporting Results: Choose Output for Humans vs Tools
Once you have filtered and shaped your data, exporting becomes straightforward. The key decision is your audience:
- Human reader: text files (reports), nicely formatted tables, or structured text intended to be read.
- Tool or automation: CSV or JSON (structured formats that other programs can reliably parse).
To keep commands reproducible, export from shaped objects (after Select-Object) and avoid exporting formatted output unless you explicitly want a “report” for reading.
Out-File: Save a Human-Readable Report
Out-File writes text to a file. It’s best for logs and reports intended for people, especially when combined with formatting.
Example: export a disk summary report
Get-PSDrive -PSProvider FileSystem | Select-Object Name, @{Name='SizeGB';Expression={ [math]::Round(($_.Used + $_.Free) / 1GB, 2) }}, @{Name='FreeGB';Expression={ [math]::Round($_.Free / 1GB, 2) }}, @{Name='PercentFree';Expression={ if (($_.Used + $_.Free) -gt 0) { [math]::Round(($_.Free / ($_.Used + $_.Free)) * 100, 1) } else { 0 } }} | Sort-Object PercentFree | Format-Table -AutoSize | Out-File -FilePath .\DiskSummary.txt -Encoding UTF8Tip: If you need a report that looks good, it’s okay to format before Out-File because you are intentionally producing text. Just remember that the result is no longer structured data.
Export-Csv: Best for Spreadsheets and Many Tools
Export-Csv writes objects as rows and properties as columns. This is ideal for Excel, reporting tools, and scripts that expect tabular data.
Example: export running services
Get-Service | Where-Object { $_.Status -eq 'Running' } | Select-Object Name, DisplayName, Status, StartType | Export-Csv -Path .\RunningServices.csv -NoTypeInformation -Encoding UTF8Example: export top CPU processes
Get-Process | Sort-Object CPU -Descending | Select-Object -First 20 Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }} | Export-Csv -Path .\TopCpuProcesses.csv -NoTypeInformation -Encoding UTF8CSV guidance:
- Use
Select-Objectto control column order and avoid exporting unnecessary properties. - CSV is flat: nested objects don’t translate well (they may become text like
System.Object[]).
ConvertTo-Json: Best for APIs and Nested Data
JSON is great when you need structured data that may include nested objects, or when sending data to web services and modern tooling.
Example: create JSON for top CPU processes
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }} | ConvertTo-JsonWrite JSON to a file
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }} | ConvertTo-Json | Out-File -FilePath .\TopCpuProcesses.json -Encoding UTF8Depth note: for nested objects, you may need a higher depth:
... | ConvertTo-Json -Depth 5Choosing the Right Export Format (Quick Guide)
| If you need… | Use… | Why |
|---|---|---|
| A readable report | Format-Table/Format-List → Out-File | Optimized for humans, not for re-import |
| Data for Excel or tabular tools | Select-Object → Export-Csv | Simple rows/columns, widely supported |
| Data for APIs, web tools, nested structures | Select-Object → ConvertTo-Json | Structured, supports nesting |
Reusable Command Patterns You Can Copy and Adapt
Top CPU Processes (Reusable Pattern)
$topCpu = Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, @{Name='CPUSeconds';Expression={ [math]::Round($_.CPU, 2) }} $topCpu | Format-Table -AutoSizeBecause the shaped data is stored in $topCpu, you can export it without rewriting the pipeline:
$topCpu | Export-Csv -Path .\TopCpu.csv -NoTypeInformation -Encoding UTF8Services by Status (Reusable Pattern)
$running = Get-Service | Where-Object { $_.Status -eq 'Running' } | Select-Object Name, DisplayName, Status, StartType $running | Format-Table -AutoSize$running | Export-Csv -Path .\RunningServices.csv -NoTypeInformation -Encoding UTF8Disk Usage Summary (Reusable Pattern)
$disk = Get-PSDrive -PSProvider FileSystem | Select-Object Name, @{Name='SizeGB';Expression={ [math]::Round(($_.Used + $_.Free) / 1GB, 2) }}, @{Name='FreeGB';Expression={ [math]::Round($_.Free / 1GB, 2) }}, @{Name='PercentFree';Expression={ if (($_.Used + $_.Free) -gt 0) { [math]::Round(($_.Free / ($_.Used + $_.Free)) * 100, 1) } else { 0 } }} $disk | Sort-Object PercentFree | Format-Table -AutoSize$disk | ConvertTo-Json -Depth 3 | Out-File -FilePath .\DiskSummary.json -Encoding UTF8