Chapter 03Lesson 04~105 minutes

Filter, Project, Sort, Group, Measure, and Enumerate

Build readable object pipelines over one coherent deployment dataset using Where-Object, Select-Object, Sort-Object, Group-Object, Measure-Object, and ForEach-Object.

BeginnerPipeline stagesData shaping

Learning objectives

By the end of this lesson

  • Explain the difference between filtering objects and projecting selected properties.
  • Use Where-Object, Select-Object, Sort-Object, Group-Object, Measure-Object, and ForEach-Object as distinct pipeline stages.
  • Explain what $_ and $PSItem refer to inside a pipeline script block.
  • Compare Where-Object script-block syntax with its simplified property syntax.
  • Build a multi-stage pipeline where each stage has an explicit input object and output object description.

1. One dataset, many questions

Instead of learning six cmdlets as unrelated syntax, use one small deployment-inventory dataset for the entire lesson. Each row-like record is a structured object representing an application environment entry. The values are intentionally deterministic so your results are easy to verify.

$apps = @(
    [pscustomobject]@{ Name='api';     Environment='prod';    Owner='platform'; Healthy=$true;  LatencyMs=118; Instances=4; Version='2.4.1' }
    [pscustomobject]@{ Name='worker';  Environment='prod';    Owner='data';     Healthy=$false; LatencyMs=420; Instances=6; Version='2.3.8' }
    [pscustomobject]@{ Name='web';     Environment='prod';    Owner='frontend'; Healthy=$true;  LatencyMs=86;  Instances=5; Version='5.1.0' }
    [pscustomobject]@{ Name='billing'; Environment='staging'; Owner='platform'; Healthy=$true;  LatencyMs=165; Instances=2; Version='1.9.4' }
    [pscustomobject]@{ Name='search';  Environment='staging'; Owner='data';     Healthy=$true;  LatencyMs=205; Instances=3; Version='3.0.2' }
    [pscustomobject]@{ Name='portal';  Environment='dev';     Owner='frontend'; Healthy=$true;  LatencyMs=72;  Instances=1; Version='5.2.0-beta' }
)

The [pscustomobject] syntax creates a record-shaped PowerShell object. Chapter 05 studies this construction technique deeply. For now, treat each entry as an object with named properties such as Name, Environment, Healthy, and LatencyMs.

$apps | Get-Member
$apps | Select-Object -First 2

2. A pipeline stage should answer one clear question

Readable pipelines behave like a sequence of small transformations. At every |, ask: what object is entering the next command, and what will that command emit?

StageQuestionTypical command
FilterWhich objects should continue?Where-Object
ProjectWhich properties/shape do I want?Select-Object
SortIn what order should the objects appear?Sort-Object
GroupWhich objects share the same key?Group-Object
MeasureWhat aggregate/count/statistic describes the values?Measure-Object
EnumerateWhat operation should run once for each incoming object?ForEach-Object

3. Filter with Where-Object: keep some objects, discard others

Filtering decides which source objects continue down the pipeline. A script block is PowerShell code inside braces. In a Where-Object script block, $_ (also available as $PSItem) refers to the current incoming object.

$prodApps = $apps | Where-Object { $_.Environment -eq 'prod' }
$prodApps | Select-Object Name, Environment, Healthy
Name   Environment Healthy
----   ----------- -------
api    prod           True
worker prod          False
web    prod           True

The script block runs against each application object. The comparison returns $true for production objects, so those objects continue. It returns $false for staging/dev objects, so those are omitted.

For simple property comparisons, Where-Object also supports a simplified syntax:

$apps | Where-Object Environment -EQ 'prod' |
    Select-Object Name, Environment

Use the simplified form for straightforward single-property tests. Use a script block when the condition combines properties, calls methods, or needs richer logic.

4. Project with Select-Object: change the shape, not the membership

Projection answers a different question from filtering. Select-Object keeps objects but emits a new shape containing the properties you request.

$apps |
    Select-Object Name, Environment, Owner, LatencyMs

If filtering is like choosing which records remain, projection is like choosing which fields each output record should contain. Do not confuse projection with formatting: selected properties remain data that can be sorted, exported, serialized, or passed to later commands.

5. Sort objects by real property values

Because LatencyMs is numeric data rather than a substring embedded in formatted text, sorting can use numeric semantics directly.

$apps |
    Sort-Object -Property LatencyMs -Descending |
    Select-Object Name, Environment, LatencyMs
Name   Environment LatencyMs
----   ----------- ---------
worker prod              420
search staging           205
billing staging          165
api    prod              118
web    prod               86
portal dev                72

You can sort on multiple properties as well, for example first by environment and then by latency. The important principle is that the sorter sees structured property values, not visual columns.

6. Group objects without destroying their detail

Group-Object partitions incoming objects by a key. The result is a set of group objects containing a Name, a Count, and the original objects in a Group property.

$byOwner = $apps | Group-Object -Property Owner
$byOwner | Select-Object Name, Count
Name     Count
----     -----
data         2
frontend     2
platform     2

Grouping does not merely print headings. It creates group objects you can inspect and process. Try $byOwner | Get-Member and then inspect $byOwner[0].Group.

7. Measure numeric properties and counts

Measure-Object can count incoming objects and calculate aggregate statistics for numeric properties.

$apps | Measure-Object

$apps | Measure-Object -Property LatencyMs -Average -Minimum -Maximum

For this deterministic dataset, the latency average is approximately 177.67 ms, the minimum is 72 ms, and the maximum is 420 ms. The result of Measure-Object is itself an object with properties such as Count, Average, Minimum, and Maximum.

8. Enumerate with ForEach-Object: run logic once per incoming object

By now you know what the current pipeline object is, so $_/$PSItem has a concrete meaning. ForEach-Object runs a script block for each incoming object.

$apps | ForEach-Object {
    "$($_.Environment)/$($_.Name) -> $($_.Version)"
}

Here, each application record becomes one string for display/logging. The original object is available as $_. $PSItem is the longer equivalent name and can improve clarity for learners:

$apps | ForEach-Object {
    [pscustomobject]@{
        Application = $PSItem.Name
        Healthy     = $PSItem.Healthy
        LatencyMs   = $PSItem.LatencyMs
    }
}

9. Compose stages into one readable operational question

Question: Which unhealthy production applications exist, ordered from highest latency to lowest, and which owner is responsible? Build the answer in stages.

$apps |
    Where-Object { $_.Environment -eq 'prod' } |
    Where-Object { -not $_.Healthy } |
    Sort-Object -Property LatencyMs -Descending |
    Select-Object Name, Owner, LatencyMs, Instances
Name   Owner LatencyMs Instances
----   ----- --------- ---------
worker data        420         6

Each stage has a single responsibility: first environment, then health, then ordering, then projection. This is longer than a compressed one-liner but much easier to review and diagnose.

10. Filter early when it improves clarity and work performed

If only production objects are relevant, removing non-production objects before expensive later stages usually reduces work. The same principle matters with large API results, files, remote inventory, and cloud resources.

Do not turn beginner code into micro-optimization contests. First write a correct, readable pipeline. Then move selective, inexpensive filters earlier when that clearly reduces downstream work. Also prefer server-side/provider-side filters when the producing command offers a reliable one, because avoiding unwanted objects at the source can be even more efficient.

11. Trace the input and output at each stage

StageInputOutput
$appsNo pipeline input; variable contains six app objects.Six app objects.
Where-Object Environment -EQ prodApp objects.Only production app objects.
Where-Object { -not $_.Healthy }Production app objects.Only unhealthy production app objects.
Sort-Object LatencyMs -DescendingUnhealthy production app objects.Same objects, ordered by latency.
Select-Object Name,Owner,LatencyMsOrdered app objects.Projected objects with three properties.

If a pipeline behaves unexpectedly, identify the first stage whose output shape or membership differs from your expectation. That localizes the problem much faster than staring at the final line.

12. Lab: build a deployment health report pipeline

Recreate the deterministic dataset, then answer these questions with pipelines: Which production apps are healthy? Which owner has the most total instances? What is the average latency by environment? Start with the guided first report and then extend it.

$apps = @(
    [pscustomobject]@{ Name='api';     Environment='prod';    Owner='platform'; Healthy=$true;  LatencyMs=118; Instances=4; Version='2.4.1' }
    [pscustomobject]@{ Name='worker';  Environment='prod';    Owner='data';     Healthy=$false; LatencyMs=420; Instances=6; Version='2.3.8' }
    [pscustomobject]@{ Name='web';     Environment='prod';    Owner='frontend'; Healthy=$true;  LatencyMs=86;  Instances=5; Version='5.1.0' }
    [pscustomobject]@{ Name='billing'; Environment='staging'; Owner='platform'; Healthy=$true;  LatencyMs=165; Instances=2; Version='1.9.4' }
    [pscustomobject]@{ Name='search';  Environment='staging'; Owner='data';     Healthy=$true;  LatencyMs=205; Instances=3; Version='3.0.2' }
    [pscustomobject]@{ Name='portal';  Environment='dev';     Owner='frontend'; Healthy=$true;  LatencyMs=72;  Instances=1; Version='5.2.0-beta' }
)

# Guided report: healthy production applications, fastest first.
$apps |
    Where-Object { $_.Environment -eq 'prod' -and $_.Healthy } |
    Sort-Object LatencyMs |
    Select-Object Name, Owner, LatencyMs, Instances

For owner totals, group first and then calculate within each group. For environment latency, group by environment and measure each group’s LatencyMs values. The lab intentionally requires you to reason about the object entering each stage rather than copy a finished answer.

Verification checklist

13. Common pipeline-shaping mistakes

Formatting too early. Keep objects as data while filtering, sorting, grouping, and measuring. Human formatting comes later.

Using Select-Object to “filter rows.” Select-Object primarily projects properties/items; use Where-Object for conditions on object values.

Using $_ without knowing its object type. Inspect the stage immediately before the script block with Get-Member when uncertain.

Writing one enormous script block. Several named pipeline stages are often easier to test and reason about than a compressed expression doing filtering, calculation, formatting, and side effects at once.

14. Knowledge check

Question 1. What is the conceptual difference between Where-Object and Select-Object?

Question 2. Inside Where-Object { ... }, what does $_ mean?

Question 3. Does Group-Object only affect display?

Question 4. Why can Sort-Object correctly order LatencyMs numerically?

Question 5. Why is filtering early often useful?

15. Summary

Object pipelines become powerful when each stage has a clear contract. Where-Object filters membership, Select-Object projects shape, Sort-Object orders, Group-Object partitions, Measure-Object aggregates, and ForEach-Object runs logic for each incoming object. Keep asking what object is flowing and keep formatting out of the data-processing stages.

16. Further reading

Next lesson

Keep formatting at the end

Lesson 05 explains the boundary between data and presentation: Format-Table, Format-List, Format-Wide, Out-Host, Out-String, calculated properties, width/truncation, and the classic failure caused by formatting before Export-Csv.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.