Skip to main content
    Courses/PowerShell/Working with Objects

    Lesson 4 • Beginner

    Working with Objects

    By the end of this lesson you'll be able to filter, sort, reshape, and summarise live system data using PowerShell's object pipeline — the one feature that makes it far more powerful than a traditional text-based shell.

    What You'll Learn

    • Why PowerShell pipes .NET objects, not raw text — and why that changes everything
    • Inspect any object's properties and methods with Get-Member
    • Pick and compute columns with Select-Object (including calculated properties)
    • Filter with Where-Object and sort with Sort-Object
    • Loop over items with ForEach-Object using $_
    • Summarise data with Group-Object and Measure-Object — no spreadsheet needed

    1. The Object Pipeline

    In a traditional shell the pipe | passes text, so the next command must re-parse that text every time. PowerShell's pipe passes objects — structured .NET values with named properties (like Name, CPU, Id) and methods. That single difference is the whole point of the language: you filter, sort, and select by property name instead of counting columns. Where-Object keeps or drops objects, Sort-Object orders them, and Select-Object picks the columns you care about. Read this worked example, then run it.

    Worked example: filter, sort, then select
    # PowerShell's killer feature: the pipeline passes OBJECTS, not text.
    # Each process is a real .NET object with named properties (Name, CPU, Id).
    # Because they're objects, you filter, sort, and pick by property — no parsing.
    
    Get-Process |
        Where-Object CPU -gt 1 |               # keep only processes using > 1s CPU
        Sort-Object CPU -Descending |          # most CPU-hungry first
        Select-Object Name, CPU                # pick just the two columns you want
    
    # $_ would be each object if you used a script block: { $_.CPU -gt 1 }
    # Here the simple "-Property -Operator value" form needs no $_ at all.
    Output
    Name              CPU
    ----              ---
    chrome      412.84375
    Code        198.39062
    pwsh         54.40625
    node         31.07812
    explorer      6.84375
    Run this in real PowerShell — Get-Process lists your machine's live processes, so your numbers will differ; the shape of the output is what matters.

    Order matters and reads left to right: filter first so later steps do less work, then sort, then select. The Where-Object CPU -gt 1 form (property, operator, value) is the easy way to filter on a single property — no $_ needed.

    2. Discover Anything with Get-Member

    You never have to guess what an object can do. Pipe any object into Get-Member (alias gm) and it lists every property and method, with types. Properties are the data (Name, CPU); methods are actions you call with () (ToUpper(), GetType()). This is the most important habit in PowerShell — when you're stuck, Get-Member tells you exactly what's available.

    Worked example: inspect, then use the properties
    # Never guess an object's properties — ask it with Get-Member (alias: gm).
    # Pipe ANY object in and it lists every Property and Method, with types.
    
    Get-Process | Get-Member -MemberType Property |
        Select-Object Name, MemberType -First 5
    
    # Now that you KNOW the property names, use them directly:
    $p = Get-Process | Select-Object -First 1
    "Name: $($p.Name)"          # $(...) runs code inside a string
    "Id:   $($p.Id)"
    "Type: $($p.GetType().Name)"  # call a METHOD with () — it's a .NET object
    Output
    Name        MemberType
    ----        ----------
    BasePriority  Property
    Container     Property
    EnableRaisingEvents Property
    Handle        Property
    HandleCount   Property
    
    Name: chrome
    Id:   1234
    Type: Process
    Run in real PowerShell; the property list comes straight from the live process object on your machine.

    Notice $(...) inside the string — that's a subexpression: it runs code and drops the result into the text. Use it whenever you need a property or a method call inside "double quotes".

    🎯 Your Turn: filter and sort by property

    The pipeline below is almost done. Fill in the two ___ blanks with the property name to filter and sort on, then run it and check your output against the expected lines.

    Fill in the blanks
    # 🎯 YOUR TURN — replace each ___ then run it.
    # A reproducible sample set so your output matches exactly.
    $files = @(
        [pscustomobject]@{ Name = 'report.docx'; SizeKB = 320 }
        [pscustomobject]@{ Name = 'photo.png';   SizeKB = 1840 }
        [pscustomobject]@{ Name = 'notes.txt';   SizeKB = 4 }
        [pscustomobject]@{ Name = 'backup.zip';  SizeKB = 9600 }
    )
    
    $files |
        Where-Object ___ -gt 300 |        # 👉 the property to filter on: SizeKB
        Sort-Object ___ -Descending |     # 👉 sort by the same property: SizeKB
        Select-Object Name, SizeKB
    
    # ✅ Expected output:
    #    Name          SizeKB
    #    ----          ------
    #    backup.zip      9600
    #    photo.png       1840
    #    report.docx      320
    Replace each ___, then run it for free at onecompiler.com/powershell.

    3. ForEach-Object and the $_ Variable

    ForEach-Object (alias %) runs a script block — code in { curly braces } — once for every object in the pipeline. Inside that block, $_ (also written $PSItem) is the current object, exactly like "this item" in a loop. The same $_ appears in any Where-Object { ... } script block too. Get comfortable with it now: $_ is everywhere in PowerShell.

    🎯 Your Turn: use $_ inside ForEach-Object

    Fill in the one blank so each price is multiplied by 1.2 (adding 20% tax). The hint tells you what ___ should be.

    Fill in the blank
    # 🎯 YOUR TURN — ForEach-Object runs a script block once per item.
    # Inside the block, $_ is the CURRENT object flowing through the pipe.
    $prices = 10, 25, 40
    
    $prices | ForEach-Object {
        $withTax = ___ * 1.2          # 👉 multiply the current item: $_
        "Price $($_) -> with tax $withTax"
    }
    
    # ✅ Expected output:
    #    Price 10 -> with tax 12
    #    Price 25 -> with tax 30
    #    Price 40 -> with tax 48
    Replace ___ with $_, then run it for free at onecompiler.com/powershell.

    4. Calculated Properties

    Select-Object doesn't only pick existing columns — it can build new ones. A calculated property is a small hashtable with two keys: Name (the column label) and Expression (a script block that computes the value, using $_ for the current object). This is how you turn raw bytes into megabytes, or a date into "days ago", right in the pipeline.

    Worked example: turn bytes into megabytes
    # Select-Object can CREATE new properties, not just pick existing ones.
    # A calculated property is a hashtable: @{ Name='Label'; Expression={ ... } }.
    $files = @(
        [pscustomobject]@{ Name = 'photo.png';  Bytes = 1572864 }
        [pscustomobject]@{ Name = 'backup.zip'; Bytes = 9830400 }
    )
    
    $files | Select-Object Name,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Bytes / 1MB, 2) } }
    
    # $_ is the current object; 1MB is a built-in PowerShell constant (1048576).
    Output
    Name        SizeMB
    ----        ------
    photo.png      1.5
    backup.zip      9.38
    This is real code — run it for free atonecompiler.com/powershellor in your own editor.

    5. Group-Object & Measure-Object

    Because the pipeline carries real data, you can do analysis without exporting to a spreadsheet. Group-Object buckets objects by a property (how many per region?). Measure-Object computes statistics in a single pass: -Sum, -Average, -Maximum, -Minimum, and a plain count. Together they answer real questions straight from the shell.

    Worked example: count groups and compute totals
    # Group-Object buckets objects by a property; Measure-Object does stats.
    $sales = @(
        [pscustomobject]@{ Region = 'North'; Amount = 100 }
        [pscustomobject]@{ Region = 'South'; Amount = 250 }
        [pscustomobject]@{ Region = 'North'; Amount = 400 }
        [pscustomobject]@{ Region = 'South'; Amount = 150 }
    )
    
    # 1) Count how many sales per region:
    $sales | Group-Object Region | Select-Object Name, Count
    
    # 2) Total, average, and max across ALL sales — one pass, no loop:
    $sales | Measure-Object Amount -Sum -Average -Maximum |
        Select-Object Count, Sum, Average, Maximum
    Output
    Name  Count
    ----  -----
    North     2
    South     2
    
    Count Sum Average Maximum
    ----- --- ------- -------
        4 900     225     400
    This is real code — run it for free atonecompiler.com/powershellor in your own editor.

    Why This Beats Text Parsing

    Here's the same job done the bash way (slicing text with awk) versus the PowerShell way (naming a property). The bash version breaks the moment a column shifts or a name contains a space; the PowerShell version can't, because it never looks at text positions at all.

    Worked example: bash text-slicing vs PowerShell objects
    # THE CONTRAST — same task, two worlds.
    
    # Bash: everything is text, so you slice columns by position and hope.
    #   ps aux | awk '{print $11, $3}' | sort -k2 -rn | head -3
    #   Break if a column shifts, a name has a space, or the header moves.
    
    # PowerShell: everything is an object, so you name what you want.
    Get-Process |
        Sort-Object CPU -Descending |
        Select-Object -First 3 Name, CPU
    
    # No awk, no column counting, no fragile text parsing. You asked for the
    # 'CPU' property by NAME — PowerShell knows its type and sorts it numerically.
    Output
    Name              CPU
    ----              ---
    chrome      412.84375
    Code        198.39062
    pwsh         54.40625
    The Get-Process half is real PowerShell — run it and compare. The commented ps aux | awk line is the bash equivalent, shown for contrast.

    Common Errors (and the fix)

    • Treating pipeline output as text. Trying to .Split() or string-match the output of Get-Process fights the language. It's already an object — use Where-Object/Select-Object on its properties instead of parsing text.
    • Accessing a property as if the object were a string. (Get-Process).ToUpper() fails because a process isn't a string — only its Name property is. Reach into the property first: (Get-Process)[0].Name.ToUpper(). When unsure, run Get-Member to see which type you actually have.
    • Forgetting $_ inside a script block. Where-Object { CPU -gt 1 } silently keeps nothing — inside { } you must write $_.CPU -gt 1. (The short form Where-Object CPU -gt 1, with no braces, needs no $_.)
    • Piping Format-Table into more commands. ... | Format-Table | Export-Csv exports formatting junk, not data. Put any Format-* or Out-* cmdlet last.
    • Quoting a number when filtering. Where-Object CPU -gt "1" can compare as text. Drop the quotes — -gt 1 — so it compares numerically.

    📋 Quick Reference

    CmdletAliasWhat it does
    Get-MembergmList an object's properties & methods
    Where-Objectwhere / ?Filter — keep objects matching a condition
    Select-ObjectselectPick/compute columns, or take -First N
    Sort-ObjectsortOrder by a property (-Descending)
    ForEach-Objectforeach / %Run a script block per item ($_)
    Group-ObjectgroupBucket objects by a property
    Measure-ObjectmeasureCount, sum, average, min, max

    Frequently Asked Questions

    Q: What does it mean that PowerShell passes objects, not text?

    When you pipe one command into another, bash hands over a plain string and the next command has to re-parse it. PowerShell hands over the actual .NET object with its named properties and methods intact, so you filter, sort, and select by property name instead of slicing text by column position.

    Q: When do I need $_ and when can I leave it out?

    Use $_ (the current pipeline object) inside a script block — anything in { curly braces }, like Where-Object { $_.CPU -gt 1 } or ForEach-Object { $_.Name }. The simpler comparison form, Where-Object CPU -gt 1, has no script block, so there is no $_ to write.

    Q: What's the difference between Select-Object and Where-Object?

    Where-Object filters rows: it keeps or drops whole objects based on a condition. Select-Object chooses columns: it picks which properties to keep (or creates new calculated ones) and can limit how many objects come through with -First. You usually filter first, then select.

    Q: How do I discover an object's property names?

    Pipe it into Get-Member (alias gm): Get-Process | Get-Member. It lists every property and method with its type. This is the single most useful command for learning what you can do with any object in the pipeline.

    Q: Why shouldn't I pipe Format-Table into another command?

    Format-* cmdlets emit display objects meant for the screen, not the original data. Anything downstream sees formatting instructions instead of real properties and breaks. Always put Format-Table, Format-List, or Out-* last in the pipeline.

    Mini-Challenge: Biggest CPU Users Report

    No blanks this time — just a brief and an outline. Build the full pipeline yourself: filter, sort, then select with a calculated Status column. Run it and check your output against the expected lines in the comments.

    🎯 Mini-Challenge: write the pipeline
    # 🎯 MINI-CHALLENGE: Biggest CPU users report
    # Use this sample data (don't change it):
    $procs = @(
        [pscustomobject]@{ Name = 'chrome'; CPU = 45.2; Threads = 60 }
        [pscustomobject]@{ Name = 'code';   CPU = 23.1; Threads = 30 }
        [pscustomobject]@{ Name = 'idle';   CPU = 0.4;  Threads = 4 }
        [pscustomobject]@{ Name = 'node';   CPU = 12.8; Threads = 18 }
    )
    #
    # 1. Keep only processes with CPU greater than 1   (Where-Object)
    # 2. Sort them by CPU, highest first               (Sort-Object -Descending)
    # 3. Show Name, CPU, and a calculated 'Status'     (Select-Object)
    #    where Status is 'HOT' if CPU -gt 20, else 'ok'
    #       @{ Name='Status'; Expression={ if ($_.CPU -gt 20) {'HOT'} else {'ok'} } }
    #
    # ✅ Expected output:
    #    Name    CPU Status
    #    ----    --- ------
    #    chrome 45.2 HOT
    #    code   23.1 HOT
    #    node   12.8 ok
    
    # your pipeline here
    Write your pipeline where the comment says, then run it for free at onecompiler.com/powershell.

    🎉 Lesson Complete!

    • ✅ The pipeline passes objects with named properties — not text to re-parse
    • Get-Member reveals any object's properties and methods
    • Where-Object filters, Sort-Object orders, Select-Object picks/computes columns
    • $_ is the current object inside any { } script block
    • Group-Object and Measure-Object do real analysis from the shell
    • Next lesson: Functions and Scripts — package these pipelines into reusable tools

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service