PowerShell is more than just a command line; it’s a powerful automation framework built on .NET. This guide covers modern operators that streamline your code, demystify the object-oriented pipeline, manage script paths for portability, leverage the full power of the .NET framework, and handle asynchronous tasks with background jobs. Whether you’re a beginner looking to build a strong foundation or an experienced scripter wanting to fill in some gaps, this guide will equip you with the knowledge to write more robust, efficient, and reliable PowerShell scripts.


Modern Operators for Cleaner Code (&&, ||, ??, ?:)

PowerShell has evolved significantly over the years, and with the release of PowerShell 7, it introduced several modern operators that make scripting more efficient, readable, and aligned with other popular programming languages like C# and JavaScript.

This section provides a deep dive into these new operators, showing how I use them to simplify my code and make my scripts more robust.

The key operators I’ll cover are:

  • && and || — Pipeline chain operators
  • ?? and ??= — Null-coalescing operators
  • ?: — Ternary operator

1. Conditional Execution: The Pipeline Chain Operators (&& and ||)

Introduced: PowerShell 7.0

These operators allow me to execute a command conditionally based on the success or failure of the previous one, just like in Bash or other shells.

  • && (AND): Executes the next command only if the previous command succeeds.
  • || (OR): Executes the next command only if the previous command fails.

In PowerShell, “success” is determined by the exit code of the previous command. A command is considered successful if its exit code is 0. Any non-zero exit code is considered a failure.

Before: The if Statement

hugo version
if ($LASTEXITCODE -eq 0) {
    wrangler pages deploy public/ --project-name=my-project
}

After: Using &&

The && operator makes this much more concise.

# This command will only run if 'hugo version' succeeds
hugo version && wrangler pages deploy public/ --project-name=my-project

Example: Handling Failures with ||

The || operator is perfect for fallback actions.

# This command will only run if 'hugo version' fails
hugo version || sudo apt install -y hugo

2. Handling null Values: The Null-Coalescing Operators (?? and ??=)

Introduced: PowerShell 7.0

These operators provide a clean and simple way to handle null values without verbose if statements.

  • ?? (Null Coalescing): Returns the value of its left-hand operand if it isn’t null; otherwise, it returns the value of the right-hand operand.
  • ??= (Null Coalescing Assignment): Assigns the value of the right-hand operand to the left-hand operand only if the left-hand operand is null.

Before: The if Statement for Null Checks

$user = Get-ADUser -Identity "PSS1001" -Properties GivenName -ErrorAction SilentlyContinue
if ($null -eq $user.GivenName) {
    $firstName = "Unknown"
} else {
    $firstName = $user.GivenName
}
Write-Host "First Name: $firstName"

After: Using ?? for Safe Defaults

$user = Get-ADUser -Identity "PSS1001" -Properties GivenName -ErrorAction SilentlyContinue
$firstName = $user.GivenName ?? "Unknown"
Write-Host "First Name: $firstName"

Example: Default Assignment with ??=

This is incredibly useful for setting default values for variables that may not have been initialized.

# If $outputPath is not already set, assign it a default value
$outputPath ??= "C:\Reports\ADUsers.csv"
Write-Host "Exporting to: $outputPath"

3. Compact if-else: The Ternary Operator (?:)

Introduced: PowerShell 7.2

The ternary operator is a compact, inline if-else statement that allows me to choose one of two values based on a condition.

Syntax: <condition> ? <if-true> : <if-false>

Before: The Multi-line if-else Block

$user = Get-ADUser -Identity "PSS1001" -Properties Enabled
if ($user.Enabled) {
    $status = "Active"
} else {
    $status = "Disabled"
}
Write-Host "User PSS1001 is $status"

After: Using the Ternary Operator

$user = Get-ADUser -Identity "PSS1001" -Properties Enabled
$status = $user.Enabled ? "Active" : "Disabled"
Write-Host "User PSS1001 is $status"

Putting It All Together: A Robust Script

These operators are most powerful when combined, allowing me to write safe, readable, and concise scripts.

This example safely gets a user, checks for null values, and uses the ternary operator to determine their status, all in a very compact block.

$user = Get-ADUser -Identity "PSS1001" -Properties GivenName, Enabled -ErrorAction SilentlyContinue

$name = $user.GivenName ?? "User not found"
$status = $user ? ($user.Enabled ? "Active" : "Disabled") : "N/A"

Write-Host "User: $name, Status: $status"

Working with Objects: The Core of PowerShell

What truly sets PowerShell apart from traditional command-line shells like Bash or Zsh is its foundational design: it’s built around objects, not text. While other shells pass streams of text between commands, PowerShell passes rich, structured objects. This fundamental difference is PowerShell’s superpower, making it an incredibly efficient and robust tool for automation, data processing, and system administration.

This section explores what it means to work with objects and how I leverage the object-oriented pipeline to write cleaner, more powerful, and more reliable scripts.

From Text Streams to Object Pipelines: The Big Shift

In a traditional shell, if I want to get a list of running processes, I might run ps aux. This command outputs a block of plain text. To get specific information, like the process ID of a single process, I have to manually parse that text with tools like grep, awk, or sed. This approach is fragile; if the output format of ps aux changes in a future OS update, my script will break.

PowerShell takes a different approach. When I run Get-Process, it doesn’t return text. It returns a collection of .NET objects (specifically, [System.Diagnostics.Process] objects). Each object is a structured bundle of information containing properties (like Id, ProcessName, and CPU) and methods (actions the object can perform).

Bash (Text-Based):

# Returns plain text that needs to be parsed
ps aux | grep 'chrome'

PowerShell (Object-Based):

# Returns a collection of process objects
Get-Process -Name 'chrome'

Because I’m working with objects, I never need to parse text. I can directly access the properties I need.

Meet My Best Friends: The Core Object Cmdlets

PowerShell’s real power comes from a set of core cmdlets designed to manipulate objects in the pipeline.

Get-Member: Discovering an Object’s Properties and Methods

Get-Member is my best friend for exploring objects. It tells me everything I need to know about an object, including its type, properties, and methods.

Get-Process -Name "powershell" | Get-Member

Select-Object: Picking the Properties I Need

Instead of parsing text columns, I can use Select-Object to pick the exact properties I want.

Get-Process | Select-Object -Property ProcessName, Id, WorkingSet

Where-Object: Filtering Objects Based on Their Properties

Where-Object allows me to filter a collection of objects based on the values of their properties.

# Find all services that are currently running
Get-Service | Where-Object -Property Status -EQ 'Running'

Sort-Object: Sorting Objects by Their Properties

I can sort any collection of objects using one or more of their properties.

# Find the top 5 most CPU-intensive processes
Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 5

Real-World Scenario: Managing Windows Services

I’ll show how these cmdlets work together in a practical scenario. I imagined I wanted to find all stopped services that are set to start automatically and then start them.

Step 1: Get all services.

Get-Service

Step 2: Filter for the services I want. I only want services that are Stopped and have a StartType of Automatic.

Get-Service | Where-Object { ($_.Status -eq 'Stopped') -and ($_.StartType -eq 'Automatic') }

Step 3: Perform an action on each service. Now, I can pipe these filtered objects to the Start-Service cmdlet.

Get-Service | Where-Object { ($_.Status -eq 'Stopped') -and ($_.StartType -eq 'Automatic') } | Start-Service -Verbose

This is the magic of the object pipeline. The Start-Service cmdlet understands the service objects passed to it and acts on them. No text parsing, no loops, just a clean, readable one-liner.

Creating My Own Objects with [PSCustomObject]

I’m not limited to the objects provided by cmdlets. I can easily create my own structured data using [PSCustomObject]. This is incredibly useful for creating custom reports or structured data to pass to other commands.

$server = [PSCustomObject]@{
    Name   = "WebServer01"
    IP     = "192.168.10.15"
    Status = "Online"
    OS     = "Windows Server 2022"
}

# The custom object can be used just like any other object
Write-Host "Pinging server $($server.Name) at IP $($server.IP)..."

The Universal Translators: ConvertTo-Json and ConvertTo-Csv

Because PowerShell works with structured objects, it can seamlessly convert that data to and from other structured formats like JSON and CSV.

# Get the top 5 processes and export them to a CSV file
Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 5 |
    Export-Csv -Path "C:\Reports\TopProcesses.csv" -NoTypeInformation

# Get a service object and convert it to JSON
Get-Service -Name "WinRM" | ConvertTo-Json

Reliable Script Paths with $PSScriptRoot & %~dp0

When writing portable scripts, one of the first challenges is locating files relative to the script itself. Whether in PowerShell or a classic Batch file, I need a reliable way to find my script’s “home” directory. This guide breaks down the two most important tools I use for this job: $PSScriptRoot for PowerShell and %~dp0 for Batch.

1. The PowerShell Method: $PSScriptRoot

PowerShell introduced $PSScriptRoot, a modern, more readable, and safer automatic variable to accomplish the same goal.

The Command

Set-Location -LiteralPath $PSScriptRoot

This command changes the current working directory to the folder containing the running PowerShell script.

Deep Dive: What is $PSScriptRoot?

$PSScriptRoot is an automatic variable populated by the PowerShell engine when a script file is executed.

| Property | Description | |:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—… [truncated]

Working with Objects: The Core of PowerShell

What truly sets PowerShell apart from traditional command-line shells like Bash or Zsh is its foundational design: it’s built around objects, not text. While other shells pass streams of text between commands, PowerShell passes rich, structured objects. This fundamental difference is PowerShell’s superpower, making it an incredibly efficient and robust tool for automation, data processing, and system administration.

This section explores what it means to work with objects and how I leverage the object-oriented pipeline to write cleaner, more powerful, and more reliable scripts.

From Text Streams to Object Pipelines: The Big Shift

In a traditional shell, if I want to get a list of running processes, I might run ps aux. This command outputs a block of plain text. To get specific information, like the process ID of a single process, I have to manually parse that text with tools like grep, awk, or sed. This approach is fragile; if the output format of ps aux changes in a future OS update, my script will break.

PowerShell takes a different approach. When I run Get-Process, it doesn’t return text. It returns a collection of .NET objects (specifically, [System.Diagnostics.Process] objects). Each object is a structured bundle of information containing properties (like Id, ProcessName, and CPU) and methods (actions the object can perform).

Bash (Text-Based):

# Returns plain text that needs to be parsed
ps aux | grep 'chrome'

PowerShell (Object-Based):

# Returns a collection of process objects
Get-Process -Name 'chrome'

Because I’m working with objects, I never need to parse text. I can directly access the properties I need.

Meet My Best Friends: The Core Object Cmdlets

PowerShell’s real power comes from a set of core cmdlets designed to manipulate objects in the pipeline.

Get-Member: Discovering an Object’s Properties and Methods

Get-Member is my best friend for exploring objects. It tells me everything I need to know about an object, including its type, properties, and methods.

Get-Process -Name "powershell" | Get-Member

Select-Object: Picking the Properties I Need

Instead of parsing text columns, I can use Select-Object to pick the exact properties I want.

Get-Process | Select-Object -Property ProcessName, Id, WorkingSet

Where-Object: Filtering Objects Based on Their Properties

Where-Object allows me to filter a collection of objects based on the values of their properties.

# Find all services that are currently running
Get-Service | Where-Object -Property Status -EQ 'Running'

Sort-Object: Sorting Objects by Their Properties

I can sort any collection of objects using one or more of their properties.

# Find the top 5 most CPU-intensive processes
Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 5

Real-World Scenario: Managing Windows Services

I’ll show how these cmdlets work together in a practical scenario. I imagined I wanted to find all stopped services that are set to start automatically and then start them.

Step 1: Get all services.

Get-Service

Step 2: Filter for the services I want. I only want services that are Stopped and have a StartType of Automatic.

Get-Service | Where-Object { ($_.Status -eq 'Stopped') -and ($_.StartType -eq 'Automatic') }

Step 3: Perform an action on each service. Now, I can pipe these filtered objects to the Start-Service cmdlet.

Get-Service | Where-Object { ($_.Status -eq 'Stopped') -and ($_.StartType -eq 'Automatic') } | Start-Service -Verbose

This is the magic of the object pipeline. The Start-Service cmdlet understands the service objects passed to it and acts on them. No text parsing, no loops, just a clean, readable one-liner.

Creating My Own Objects with [PSCustomObject]

I’m not limited to the objects provided by cmdlets. I can easily create my own structured data using [PSCustomObject]. This is incredibly useful for creating custom reports or structured data to pass to other commands.

$server = [PSCustomObject]@{
    Name   = "WebServer01"
    IP     = "192.168.10.15"
    Status = "Online"
    OS     = "Windows Server 2022"
}

# The custom object can be used just like any other object
Write-Host "Pinging server $($server.Name) at IP $($server.IP)..."

The Universal Translators: ConvertTo-Json and ConvertTo-Csv

Because PowerShell works with structured objects, it can seamlessly convert that data to and from other structured formats like JSON and CSV.

# Get the top 5 processes and export them to a CSV file
Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 5 |
    Export-Csv -Path "C:\Reports\TopProcesses.csv" -NoTypeInformation

# Get a service object and convert it to JSON
Get-Service -Name "WinRM" | ConvertTo-Json

Reliable Script Paths with $PSScriptRoot & %~dp0

When writing portable scripts, one of the first challenges is locating files relative to the script itself. Whether in PowerShell or a classic Batch file, I need a reliable way to find my script’s “home” directory. This guide breaks down the two most important tools I use for this job: $PSScriptRoot for PowerShell and %~dp0 for Batch.

1. The PowerShell Method: $PSScriptRoot

PowerShell introduced $PSScriptRoot, a modern, more readable, and safer automatic variable to accomplish the same goal.

The Command

Set-Location -LiteralPath $PSScriptRoot

This command changes the current working directory to the folder containing the running PowerShell script.

Deep Dive: What is $PSScriptRoot?

$PSScriptRoot is an automatic variable populated by the PowerShell engine when a script file is executed.

| Property | Description | |:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—|:—… [truncated]