Skip to content

Object Manipulation

Note

Master PowerShell object manipulation with Select-Object, Where-Object, ForEach-Object, calculated properties, and more.

Overview

PowerShell works with objects, not text. Every cmdlet returns objects with properties and methods. Mastering object manipulation is the key to unlocking PowerShell's full potential. This is what makes PowerShell different from traditional shells - you don't parse text, you work with structured data.

Understanding PowerShell Objects

What is an Object?

An object is a structured piece of data with:

  • Properties: Data about the object (like Name, Size, Status)
  • Methods: Actions you can perform on the object (like Kill, Start, Stop)
# Get a process object
$proc = Get-Process | Select-Object -First 1

# Properties - data about the object
$proc.Name          # The process name
$proc.CPU           # CPU time used
$proc.WS            # Working set (memory)

# Methods - actions on the object
$proc | Get-Member -MemberType Method
$proc.Kill()        # Kill the process

Exploring Objects

# See all properties and methods
Get-Process | Get-Member

# Just properties
Get-Process | Get-Member -MemberType Property

# Just methods
Get-Process | Get-Member -MemberType Method

# Get type name
(Get-Process).GetType().FullName    # System.Diagnostics.Process

Select-Object (Choose Properties)

Select which properties to display and work with:

Basic Selection

# Select specific properties
Get-Process | Select-Object Name, CPU, WS

# Select first N items
Get-Process | Select-Object -First 10

# Select last N items
Get-ChildItem | Select-Object -Last 5

# Skip and select
Get-Process | Select-Object -Skip 10 -First 5   # Skip first 10, get next 5

# Get unique values
1,2,2,3,3,3,4 | Select-Object -Unique            # 1,2,3,4

Expand Property

# Get just the values (not an object with that property)
Get-Process | Select-Object -ExpandProperty Name
# Returns: "chrome", "powershell", "code", etc.

# vs regular select (returns objects)
Get-Process | Select-Object Name
# Returns: @{Name="chrome"}, @{Name="powershell"}, etc.

# Useful for getting a simple list
$processNames = Get-Process | Select-Object -ExpandProperty Name
# $processNames is now a simple string array

Calculated Properties

Create new properties on-the-fly:

# Basic calculated property
Get-Process | Select-Object Name, @{Name="MemoryMB";Expression={$_.WS/1MB}}

# Multiple calculated properties
Get-ChildItem | Select-Object Name,
    @{Name="SizeMB";Expression={[math]::Round($_.Length/1MB, 2)}},
    @{Name="Modified";Expression={$_.LastWriteTime.ToString("yyyy-MM-dd")}}

# Shorter syntax (Label or L instead of Name, E instead of Expression)
Get-Process | Select-Object Name,
    @{L="MemoryMB";E={[math]::Round($_.WS/1MB, 2)}},
    @{L="CPUTime";E={[math]::Round($_.CPU, 2)}}

Where-Object (Filter Objects)

Filter objects based on conditions:

Basic Filtering

# Long syntax (explicit)
Get-Process | Where-Object {$_.CPU -gt 100}
Get-Service | Where-Object {$_.Status -eq "Running"}
Get-ChildItem | Where-Object {$_.Length -gt 1MB}

# Short syntax (PowerShell 3.0+)
Get-Process | Where-Object CPU -GT 100
Get-Service | Where-Object Status -EQ "Running"
Get-ChildItem | Where-Object Length -GT 1MB

Complex Filtering

# Multiple conditions with -and
Get-Process | Where-Object {($_.CPU -gt 50) -and ($_.WS -gt 100MB)}

# Multiple conditions with -or
Get-Service | Where-Object {($_.Status -eq "Running") -or ($_.StartType -eq "Automatic")}

# Negation with -not
Get-ChildItem | Where-Object {-not ($_.PSIsContainer)}  # Files only

# String matching
Get-Process | Where-Object {$_.Name -like "chrome*"}
Get-Service | Where-Object {$_.DisplayName -match "Windows"}

Common Filter Patterns

# Not null
Get-Process | Where-Object {$null -ne $_.Company}

# Exists in array
$allowedUsers = "admin", "user1", "user2"
Get-LocalUser | Where-Object {$_.Name -in $allowedUsers}

# Property exists and has value
Get-Process | Where-Object {$_.Description -and $_.Description.Length -gt 0}

# Multiple property checks
Get-ChildItem | Where-Object {
    ($_.Extension -in @(".txt", ".log")) -and
    ($_.LastWriteTime -lt (Get-Date).AddDays(-30))
}

ForEach-Object (Process Each Item)

Perform actions on each object in the pipeline:

Basic Usage

# Long syntax
Get-Process | ForEach-Object {
    Write-Output "$($_.Name) is using $($_.WS) bytes"
}

# Short syntax with member access
Get-Process | ForEach-Object Name                    # Just get the Name property
Get-Process | ForEach-Object {$_.Name}              # Same thing

Performing Actions

# Modify each object
"file1.txt", "file2.txt" | ForEach-Object {
    New-Item -Path $_ -ItemType File
    Write-Output "Created $_"
}

# Call methods
Get-Service | Where-Object {$_.Name -like "test*"} | ForEach-Object {
    $_.Stop()
    Write-Output "Stopped $($_.Name)"
}

# Complex processing
Get-ChildItem -Filter "*.log" | ForEach-Object {
    $content = Get-Content -Path $_.FullName
    $errorCount = ($content | Select-String "ERROR").Count

    [PSCustomObject]@{
        File = $_.Name
        Errors = $errorCount
        Size = $_.Length
    }
}

Begin, Process, End Blocks

# Advanced ForEach-Object with three sections
Get-Process | ForEach-Object -Begin {
    # Runs once before processing
    $total = 0
    Write-Output "Starting..."
} -Process {
    # Runs for each item
    $total += $_.WS
} -End {
    # Runs once after all items
    Write-Output "Total memory: $($total/1GB) GB"
}

Group-Object (Group by Property)

Group objects by a property value:

# Basic grouping
Get-Service | Group-Object -Property Status

# Output shows:
# Count Name   Group
# ----- ----   -----
#   150 Running {System.ServiceProcess.ServiceController, ...}
#    50 Stopped {System.ServiceProcess.ServiceController, ...}

# Group by multiple properties
Get-Process | Group-Object -Property Company, Name

# Group files by extension
Get-ChildItem | Group-Object -Property Extension |
    Select-Object Name, Count |
    Sort-Object -Property Count -Descending

Working with Groups

# Get the groups
$grouped = Get-Process | Group-Object -Property Company

# Access individual groups
$grouped | ForEach-Object {
    Write-Output "$($_.Name): $($_.Count) processes"
}

# Get items in a specific group
$msProcesses = ($grouped | Where-Object {$_.Name -eq "Microsoft Corporation"}).Group

Measure-Object (Calculate Statistics)

Calculate statistics on object properties:

# Count items
Get-ChildItem | Measure-Object
# Returns Count

# Sum a property
Get-ChildItem | Measure-Object -Property Length -Sum
# Returns Count and Sum

# Get statistics
1..100 | Measure-Object -Average -Sum -Maximum -Minimum
# Returns Count, Average, Sum, Maximum, Minimum

# Measure strings
Get-Content file.txt | Measure-Object -Line -Word -Character

Sort-Object (Sort by Property)

Sort objects by one or more properties:

# Basic sorting (ascending)
Get-Process | Sort-Object -Property CPU
Get-ChildItem | Sort-Object -Property Length

# Descending
Get-Process | Sort-Object -Property CPU -Descending

# Multiple properties
Get-Process | Sort-Object -Property Company, Name
Get-ChildItem | Sort-Object -Property Extension, Name

# Custom sort logic
Get-Process | Sort-Object -Property @{Expression={$_.WS}; Descending=$true}, Name

Compare-Object (Compare Two Sets)

Compare two collections of objects:

# Basic comparison
$before = Get-Process
Start-Process notepad
Start-Sleep -Seconds 2
$after = Get-Process

Compare-Object -ReferenceObject $before -DifferenceObject $after -Property Name

# Find differences in arrays
$list1 = "apple", "banana", "cherry"
$list2 = "banana", "cherry", "date"
Compare-Object $list1 $list2

# Output:
# InputObject SideIndicator
# ----------- -------------
# apple       <=              (only in list1)
# date        =>              (only in list2)

# Find only items in second list
Compare-Object $list1 $list2 | Where-Object {$_.SideIndicator -eq "=>"}

Real-World Examples

Example: Process Report with Calculated Properties

Scenario: Generate a detailed process report with custom formatting

Get-Process | Where-Object {$_.CPU -gt 0} |
    Sort-Object -Property CPU -Descending |
    Select-Object -First 20 Name,
        @{L="CPU(s)";E={[math]::Round($_.CPU, 2)}},
        @{L="Memory(MB)";E={[math]::Round($_.WS/1MB, 2)}},
        @{L="Handles";E={$_.Handles}},
        @{L="StartTime";E={$_.StartTime.ToString("yyyy-MM-dd HH:mm")}},
        @{L="Runtime";E={
            if ($_.StartTime) {
                $runtime = (Get-Date) - $_.StartTime
                "$($runtime.Days)d $($runtime.Hours)h $($runtime.Minutes)m"
            } else { "N/A" }
        }} |
    Format-Table -AutoSize

Example: File System Analysis

Scenario: Analyze file system usage by type

# Get all files recursively
$files = Get-ChildItem -Path C:\Projects -Recurse -File

# Group by extension and calculate totals
$analysis = $files | Group-Object -Property Extension | ForEach-Object {
    $totalSize = ($_.Group | Measure-Object -Property Length -Sum).Sum

    [PSCustomObject]@{
        Extension = if ($_.Name) { $_.Name } else { "(no extension)" }
        FileCount = $_.Count
        TotalSizeMB = [math]::Round($totalSize/1MB, 2)
        AverageSizeMB = [math]::Round(($totalSize/$_.Count)/1MB, 2)
        LargestFile = ($_.Group | Sort-Object Length -Descending | Select-Object -First 1).Name
    }
}

# Display sorted by total size
$analysis | Sort-Object -Property TotalSizeMB -Descending | Format-Table -AutoSize

Example: Service Status Dashboard

Scenario: Create a service status summary with grouping

# Get services grouped by status
$serviceStats = Get-Service | Group-Object -Property Status | ForEach-Object {
    [PSCustomObject]@{
        Status = $_.Name
        Count = $_.Count
        Services = $_.Group.Name -join ", "
    }
}

# Find automatic services that are stopped
$stoppedAutomatic = Get-Service |
    Where-Object {($_.StartType -eq "Automatic") -and ($_.Status -eq "Stopped")} |
    Select-Object Name, DisplayName, Status, StartType

# Display results
Write-Output "`n===== Service Summary ====="
$serviceStats | Format-Table -AutoSize

if ($stoppedAutomatic) {
    Write-Warning "`nAutomatic services that are stopped:"
    $stoppedAutomatic | Format-Table -AutoSize
}

Example: Log File Error Analysis

Scenario: Parse log files and count errors by type

# Get log files
$logFiles = Get-ChildItem -Path C:\Logs -Filter "*.log"

# Process each log and extract errors
$errors = $logFiles | ForEach-Object {
    $content = Get-Content -Path $_.FullName
    $errorLines = $content | Select-String "ERROR"

    $errorLines | ForEach-Object {
        # Extract error type (assume format: "ERROR: [Type] Message")
        if ($_.Line -match "ERROR:\s*\[(.+?)\]") {
            [PSCustomObject]@{
                File = $_.Filename
                Line = $_.LineNumber
                ErrorType = $matches[1]
                Message = $_.Line
            }
        }
    }
}

# Group errors by type
$errorSummary = $errors | Group-Object -Property ErrorType |
    Select-Object @{L="ErrorType";E={$_.Name}}, Count |
    Sort-Object -Property Count -Descending

Write-Output "`n===== Error Summary ====="
$errorSummary | Format-Table -AutoSize

Write-Output "`n===== Top 10 Most Recent Errors ====="
$errors | Select-Object -Last 10 | Format-Table File, ErrorType, Line

Combining Commands

Common Pipeline Patterns

# Filter → Sort → Select → Format
Get-Process |
    Where-Object {$_.CPU -gt 10} |
    Sort-Object -Property CPU -Descending |
    Select-Object -First 10 Name, CPU, WS |
    Format-Table -AutoSize

# Get → Group → Measure → Select
Get-ChildItem -Recurse |
    Group-Object -Property Extension |
    ForEach-Object {
        [PSCustomObject]@{
            Extension = $_.Name
            Count = $_.Count
            TotalSize = ($_.Group | Measure-Object Length -Sum).Sum
        }
    } |
    Sort-Object -Property TotalSize -Descending

# Filter → ForEach → Create Objects
Get-Service |
    Where-Object {$_.Status -ne "Running"} |
    ForEach-Object {
        [PSCustomObject]@{
            Service = $_.Name
            Status = $_.Status
            StartType = $_.StartType
            Action = if ($_.StartType -eq "Automatic") { "Should be running!" } else { "OK" }
        }
    }

Tips & Tricks

Use -ExpandProperty for Simple Lists

# Returns objects with Name property
Get-Process | Select-Object Name

# Returns simple string array
Get-Process | Select-Object -ExpandProperty Name

# Use -ExpandProperty when you need a simple list for other commands
$names = Get-Process | Select-Object -ExpandProperty Name
$names -contains "chrome"  # Works with simple array

Calculated Properties for Formatting

# Good for making data readable
Get-Process | Select-Object Name,
    @{L="MemoryGB";E={"{0:N2}" -f ($_.WS/1GB)}},
    @{L="CPU%";E={"{0:P0}" -f ($_.CPU/100)}}

Group-Object with Hash Tables

# Convert groups to hash table for quick lookup
$servicesByStatus = @{}
Get-Service | Group-Object Status | ForEach-Object {
    $servicesByStatus[$_.Name] = $_.Group
}

# Now quick access
$runningServices = $servicesByStatus["Running"]

Where-Object vs. Where() Method

# Where-Object (pipeline, processes one at a time)
Get-Process | Where-Object {$_.CPU -gt 100}

# .Where() method (faster, requires full collection first)
(Get-Process).Where({$_.CPU -gt 100})

# Use Where-Object in pipelines, .Where() for speed with arrays

Select-Object Doesn't Change Original

$processes = Get-Process
$selected = $processes | Select-Object Name, CPU

# $selected only has Name and CPU properties
# $processes still has all original properties

# Select-Object creates NEW objects, doesn't modify originals

Calculated Properties Performance

# Don't calculate same thing repeatedly
# BAD:
Get-ChildItem | Select-Object Name,
    @{L="SizeMB";E={$_.Length/1MB}},
    @{L="SizeKB";E={$_.Length/1KB}}  # Accesses Length twice

# Better: Calculate once if needed in script
$files = Get-ChildItem
$files | ForEach-Object {
    $sizeBytes = $_.Length
    [PSCustomObject]@{
        Name = $_.Name
        SizeMB = $sizeBytes/1MB
        SizeKB = $sizeBytes/1KB
    }
}

Additional Resources