Attribution

This introduction to PowerShell is based on notes we took while studying the excellent PowerShell For Beginners video course by Jason Helmick and Jeffrey Snover.

We highly recommend this course as a starting point for learning PowerShell; it was instrumental in our understanding of the subject. A big thank you to the creators for providing such valuable resources!

Don’t Fear The Shell

PowerShell might look intimidating at first, but it is designed to be approachable. Let us start with the basics.

Getting the Current Version

To check which version of PowerShell you are running:

$PSVersionTable

Copy and Paste in PowerShell

PowerShell handles copy and paste a bit differently than you might expect:

  • Highlight the text you want to copy, then right-click to copy it.
  • Right-click again to paste at the cursor position.

Cmdlets Structure

PowerShell commands follow a simple pattern: Verb-Noun. This makes them easy to remember and guess.

  • Format: <verb>-<noun>
  • Example: Get-Service, Stop-Process, Set-Location

Running Multiple Commands

You can run multiple commands on a single line using the semicolon (;) as a separator:

# ; is a statement separator
cls; Get-Alias

Common Aliases and Linux Commands

PowerShell includes aliases to make the transition easier for people coming from Linux or CMD:

What you typeWhat it actually runs
historyGet-History
cdSet-Location
ls or dirGet-ChildItem
clear or clsClear-Host
mkdir or mdNew-Item -ItemType Directory
man or helpGet-Help
cat or typeGet-Content

To find aliases for a specific command:

# List all aliases
Get-Alias

# Find aliases for a specific command
Get-Alias -Definition Get-Process

# Find aliases matching a pattern
Get-Alias *sv

Running Windows Native Commands

You can still launch Windows applications directly from PowerShell:

calc
notepad
mspaint

Windows Networking Commands

Standard networking commands work too:

ping google.com
ipconfig /all

Comments

Add comments to your code using the hash symbol:

# This is a test comment
Get-Process  # Comments can go at the end of lines too

Wildcards

gal *sv

The Help System

PowerShell has a built-in help system that is your best friend. Learn to use it early and often.

Update the Help System

Help files are not included by default on some systems. Update them first:

Update-Help
Update-Help -Force

Read the Conceptual Topics

PowerShell includes detailed documentation on concepts, not just commands:

# Get help on a specific topic
Get-Help about_EventLogs

# List all available conceptual topics
Get-Help about_*

Get All Available Verbs

To see what verbs are available for naming your own functions:

Get-Verb

# Count them
Get-Verb | Measure-Object

Get Detailed Information About a Command

The Get-Help command has several useful switches:

# Get detailed info
Get-Help Get-Service -Detailed

# Get only examples (very useful!)
Get-Help Get-Service -Examples

# Get everything including parameter details
Get-Help Get-Service -Full

# Show help in a separate popup window
Get-Help Get-Service -ShowWindow

# Open online documentation (great for older PowerShell versions)
Get-Help Get-Service -Online

Understanding Command Syntax

When you look at help output, you will see parameter sets. Here is how to read them:

  • - (dash) precedes parameter names.
  • [] (square brackets) mean the parameter is optional.
  • <> (angle brackets) indicate where you put values.
  • String[] means the parameter accepts multiple string values, separated by commas.

Positional parameters ([[-Name] <String[]>]) let you skip the parameter name if you put values in the right order:

# Explicit
Get-Service -Name bits

# Positional (same result)
Get-Service bits

# Multiple values
Get-Service -Name bits, bfe
Get-Service -Name b*, c*

Example: Get the Newest Error Event Logs

Get-EventLog -LogName System -Newest 3 -EntryType Error

The Pipeline: Getting Connected

The pipeline is one of PowerShell’s most powerful features. It lets you chain commands together to perform complex operations simply.

Introduction to the Pipeline

The basic idea: take objects (not just text), send them through the pipeline using the pipe character |, and let each command process them.

Get-Service | Select-Object Name, Status | Sort-Object Name

# Same thing, formatted for readability
Get-Service |
    Select-Object Name, Status |
    Sort-Object Name

The Current Object in the Pipeline

When processing items in the pipeline, use $_ or $PSItem to refer to the current object:

Get-Service | Where-Object { $_.Status -eq "Running" }

Example: Working with Services

# Stop a specific service
Get-Service -Name bits | Stop-Service

# Restart a service and display the results
Get-Service -Name bits | Start-Service -PassThru

# Export services to a CSV file
Get-Service | Export-Csv -Path C:\service.csv

# Import it back
Import-Csv C:\service.csv

Example: Debugging Processes

Compare running processes against a baseline:

# Take a snapshot on a baseline machine
Get-Process | Export-Clixml -Path C:\good.xml

# Compare with the machine you are debugging
Compare-Object -ReferenceObject (Import-Clixml 'C:\good.xml') -DifferenceObject (Get-Process) -Property Name

ConvertTo vs Export

Here is the difference:

  • Export commands convert data and write it to a file.
  • ConvertTo commands only convert the data; you need to pipe it to Out-File yourself.

Example: Export Services to HTML

Get-Service | ConvertTo-Html -Property Name, Status | Out-File C:\test.html

# Open the file
C:\test.html

Safety First: WhatIf

When you are unsure what a command will do, use -WhatIf. It shows exactly what would happen without actually doing it:

Get-Service | Stop-Service -WhatIf

Confirming Actions

Use -Confirm to see what a command would do and get prompted before it runs:

Get-Service | Stop-Service -Confirm

# Narrow it down to save time
Get-Service -DisplayName *bi* | Stop-Service -Confirm

Extending the Shell

Adding Packages via GUI

You can manage snap-ins through the Microsoft Management Console:

mmc
# Then go to File -> Add/Remove Snap-ins

Importing Modules via PowerShell

Modules extend PowerShell’s capabilities. Here is how to work with them:

# See available modules
Get-Module -ListAvailable

# See currently loaded modules
Get-Module

# Import a module
Import-Module ServerManager

# List commands in a module
Get-Command -Module Server*

# Example: Working with Windows features
Get-WindowsFeature
Add-WindowsFeature Telnet-Client, Telnet-Server -Restart

Objects for the Admin

Recommended Resources

  • Website: powershell.org
  • Book: Learn Windows PowerShell in a Month of Lunches by Don Jones (Chapter 9 is especially helpful)

Discovering Object Properties and Methods

Everything in PowerShell is an object. To see what you can do with it:

Get-Process | Get-Member
# or shorthand
Get-Process | gm

What Is an Object?

  • Properties: Things the object has (data).
  • Methods: Things the object can do (actions).

Example: Processes with High Handle Counts

Get-Process | Where-Object Handles -GT 1000 | Sort-Object Handles

Example: Display Folder Contents Sorted by Size

Get-ChildItem | Select-Object -Property Name, Length | Sort-Object -Property Length -Descending

Example: Selective Event Log Export

Get-EventLog -LogName System -Newest 5 |
    Select-Object -Property EventID, TimeWritten, Message |
    Sort-Object -Property TimeWritten |
    ConvertTo-Html |
    Out-File C:\errorlog.html

Example: Working with XML Files

PowerShell can cast files as XML objects and navigate them like objects:

# Read an XML file
$x = [xml](Get-Content .\r_and_j.xml)

# Display the object
$x

# Check the type
$x.GetType()

# Navigate the XML structure
$x.Play.Act[0].Scene[0].Speech

# Group and analyze
$x.Play.Act.Scene.Speech | Group-Object Speaker | Sort-Object Count

Filtering with Where-Object

There are two ways to use Where-Object:

Simple syntax (PowerShell 3.0 and later):

Get-Process | Where-Object Handles -GE 1000

Script block syntax (for complex conditions):

Get-Service | Where-Object { $_.Status -eq "Running" }
Get-Service | Where-Object { $PSItem.Status -eq "Running" -and $_.Name -like "b*" }

How it works:

  1. Each object coming through the pipeline gets assigned to $_ or $PSItem.
  2. PowerShell evaluates the code inside {}.
  3. If the code returns $true, the object passes through. If $false, it is discarded.

Available Operators

To see all available comparison operators:

Get-Help *operators*

The Pipeline Deeper: How It Really Works

Understanding how the pipeline connects commands helps you troubleshoot when things do not work as expected. There are four ways data flows through the pipeline.

Step One: ByValue Matching

The receiving command looks for a parameter that accepts pipeline input ByValue and matches the object type exactly.

# Get-Service outputs ServiceController objects
# Stop-Service accepts ServiceController[] via -InputObject (ByValue)
Get-Service | Stop-Service -WhatIf

To check if types match:

# See what type a command outputs
<command> | Get-Member

# Check what types a receiving command accepts
Get-Help <receiving-command> -Full
# Look for "Accepts pipeline input: True (ByValue)"

Step Two: ByPropertyName Matching

The receiving command matches property names from the incoming objects to its parameter names.

# ByPropertyName: Name -> Name
Get-Service | Stop-Process -WhatIf

# ByPropertyName: Path parameter matches Path property
Get-Process calc | Get-ChildItem

Step Three: Customizing Property Names

When property names do not match, fix them on the fly using calculated properties:

# Problem: Get-ADComputer outputs "Name", but Get-Service needs "ComputerName"
Get-ADComputer -Filter * | Get-Service -Name bits  # This fails

# Solution: Rename the property using a calculated property
Get-ADComputer -Filter * |
    Select-Object -Property @{Name='ComputerName'; Expression={$_.Name}} |
    Get-Service -Name bits

# Shorthand versions work too
Get-ADComputer -Filter * | Select-Object -Property @{N='ComputerName'; E={$_.Name}}
Get-ADComputer -Filter * | Select-Object -Property @{L='ComputerName'; E={$_.Name}}

Step Four: The Parenthetical Approach

When pipeline binding fails entirely, use parentheses to pass data as a parameter:

# -ComputerName expects strings, not objects
Get-WmiObject -Class Win32_Bios -ComputerName (Get-ADComputer -Filter * | Select-Object -ExpandProperty Name)

# PowerShell 3.0 shortcut
Get-WmiObject -Class Win32_Bios -ComputerName (Get-ADComputer -Filter *).Name

# Using script blocks (also works)
Get-ADComputer -Filter * | ForEach-Object {
    Get-WmiObject -Class Win32_Bios -ComputerName $_.Name
}

The Power in the Shell: Remoting

PowerShell Remoting lets you manage remote computers as easily as local ones. It uses the WS-Management protocol (implemented as WinRM).

Advantages of WinRM:

  • Uses a single port (easy to firewall)
  • Secure by default (encrypted with Kerberos)
  • Fast performance
  • Optional SSL for double encryption

Enable Remoting

Remoting is enabled by default on Server 2012 and later. For other systems:

Via PowerShell (per machine):

Enable-PSRemoting

Via Group Policy (for many machines):

Navigate to: Computer Configuration/Policies/Administrative Templates/Windows Components/Windows Remote Management (WinRM)/WinRM Service

One-to-One Connections

Enter an interactive session on a remote computer:

Enter-PSSession -ComputerName DC

# When done, return to your local session
Exit-PSSession

# Or open Remote Desktop
mstsc /v:DC

One-to-Many Connections

Run commands on multiple computers simultaneously:

# Run a command on multiple computers
Invoke-Command -ComputerName DC, S1, S2 { Get-EventLog -LogName System -Newest 3 }

# Format the output nicely
Invoke-Command -ComputerName DC, S1, S2 { Get-EventLog -LogName System -Newest 3 } |
    Sort-Object TimeWritten |
    Format-Table -Property TimeWritten, Message -AutoSize

# Restart multiple systems
Invoke-Command -ComputerName DC, S1, S2 { Restart-Computer }

# Check service status remotely
Invoke-Command -ComputerName DC, S1, S2 { Get-Service -Name bits }

What Happens During Remoting

When you run Invoke-Command, here is what happens behind the scenes:

  1. Establish a TCP connection
  2. Start a PowerShell process on the remote machine
  3. Load the .NET Framework
  4. Initialize PowerShell
  5. Run profile scripts
  6. Send your code across the network
  7. Execute the code on the remote system
  8. Serialize the results into XML
  9. Transmit the XML back
  10. Deserialize (reconstruct) the objects on your local machine

Understanding Serialization

Serialization converts live objects into property bags (XML) for transport, then reconstructs them on the other side. Only properties survive; methods are lost.

# Local object - has methods
Get-Service -Name bits | Get-Member
# Type: ServiceController

# Remote object - deserialized, properties only
Invoke-Command -ComputerName S1 { Get-Service -Name bits } | Get-Member
# Type: Deserialized.ServiceController

Windows PowerShell Web Access

Set up web-based PowerShell access:

# Install the feature (also installs IIS and .NET 4.5)
Install-WindowsFeature WindowsPowerShellWebAccess

# See available commands
Get-Help *Pswa*

# Install the web application (uses test certificate for 90 days)
Install-PswaWebApplication -UseTestCertificate

# Add authorization rules (who can access what)
Add-PswaAuthorizationRule -UserName Domain\User -ComputerName Server01 -ConfigurationName AdminsOnly

# Testing only: Allow everyone access to everything (NOT for production!)
Add-PswaAuthorizationRule * * *

Example: Find the Server with the Most Free Space

# Check disk space on multiple servers
Invoke-Command DC, S1, S2 { Get-Volume } | Sort-Object SizeRemaining

# Get the top 3 with most free space
Invoke-Command DC, S1, S2 { Get-Volume } | Sort-Object SizeRemaining | Select-Object -Last 3

Getting Prepared for Automation

PowerShell Security Goals

PowerShell is designed to be secure by default:

  • No script execution allowed initially
  • .ps1 files open in Notepad by default (do not run)
  • You must type the full path or .\ to run a script

Execution Policy

The execution policy controls what scripts can run. It applies to scripts, not interactive commands.

Check and set your policy:

Get-ExecutionPolicy
Set-ExecutionPolicy RemoteSigned

Policy options:

  • Restricted (default) - No scripts allowed
  • AllSigned - All scripts must be digitally signed
  • RemoteSigned - Downloaded scripts must be signed; local scripts can run
  • Unrestricted - All scripts run (not recommended)
  • Bypass - Nothing is blocked (use with caution)

Working with Certificates

Create a self-signed certificate for testing:

# Create the certificate
New-SelfSignedCertificate -CertStoreLocation Cert:\CurrentUser\My -Subject "CN=PowerShell Code Signing" -Type CodeSigningCert

# View certificate drives
Get-PSDrive

# Get code signing certificates
$cert = Get-ChildItem Cert:\CurrentUser -Recurse -CodeSigningCert | Select-Object -First 1

Sign Your Scripts

# Set policy to require signatures
Set-ExecutionPolicy AllSigned

# Sign a script
Set-AuthenticodeSignature -Certificate $cert -FilePath .\test.ps1

# Run it (you will be prompted to trust the certificate)
.\test.ps1

Variables

Variables in PowerShell start with $:

# Simple variable
$MyVar = "Hello"
$MyVar

# Variable containing an object
$MyVar = Get-Service bits
$MyVar.Status    # Access properties
$MyVar.Stop()     # Call methods
$MyVar.Refresh()  # Refresh the object

# Variable names can contain spaces and special characters (use braces)
${THIS IS A TEST} = 4
${THIS IS A TEST}

# Special characters are allowed too
${MY VAR 123!@#} = "Amazing"
${MY VAR 123!@#}

Reading User Input

# Prompt for input
$computer = Read-Host "Enter a ComputerName"

# Use the variable
Get-Service -Name bits -ComputerName $computer

Writing to Files

# Create a range and display it
1..5

# Write to a file
1..5 > test.txt

# Read it back using variable syntax
${C:\path\test.txt}

# Explanation: The `$` syntax indicates where data is stored.
# Normally in memory (lost when the process ends), but files persist.

Writing Output

Avoid Write-Host because it does not send data to the pipeline – it only displays to the screen. If you must use it, consider it “show” output only.

# Display output (does not go to pipeline)
Write-Host $var -ForegroundColor Red -BackgroundColor Green

# This fails because Write-Host produces no output for the pipeline
Write-Host $var | Get-Member

# Use Write-Output instead (creates objects)
Write-Output $var | Get-Member

# Warning and error messages
Write-Warning "Please do not do that"
Write-Error "Something went wrong!"

Automation at Scale: Persistent Sessions

The Problem with One-Off Commands

Creating and tearing down connections for every command is inefficient. It takes time to:

  • Establish TCP connections
  • Start processes
  • Load the CLR and PowerShell
  • Run profiles
  • Execute code
  • Tear everything down

Example: The Problem

# Create a variable on a remote machine
Invoke-Command -ComputerName DC { $var = 2 }

# Try to read it back (fails because the session is gone)
Invoke-Command -ComputerName DC { Write-Output $var }
# No output - the session was destroyed

Solution: Persistent Sessions

# Create a persistent session
$session = New-PSSession -ComputerName DC

# View open sessions
Get-PSSession

# Now variables persist
Invoke-Command -Session $session { $var = 2 }
Invoke-Command -Session $session { $var }  # Returns 2

Performance Comparison

# One-off connection (slower)
Measure-Command { Invoke-Command -ComputerName DC { Get-Process } }

# Reusing a session (faster)
Measure-Command { Invoke-Command -Session $session { Get-Process } }

Practical Example: Deploying Web Servers

# Define servers
$servers = 'S1', 'S2'

# Verify no websites exist yet (will show errors - expected)
$servers | ForEach-Object { Start-Process iexplore "http://$_" }

# Create persistent sessions
$sessions = New-PSSession -ComputerName $servers

# Install web server feature (runs in parallel on all machines)
Invoke-Command -Session $sessions { Install-WindowsFeature Web-Server }

# Verify websites are running
$servers | ForEach-Object { Start-Process iexplore "http://$_" }

# Create a simple webpage
notepad C:\default.htm

# Deploy the webpage to all servers
$servers | ForEach-Object {
    Copy-Item C:\default.htm -Destination "\\$_\C$\inetpub\wwwroot"
}

# Verify deployment
$servers | ForEach-Object { Start-Process iexplore "http://$_" }

Implicit Remoting: Using Remote Modules Locally

You can import modules from remote machines without installing them locally:

# Create a session to a domain controller
$dcSession = New-PSSession -ComputerName DC

# Import the ActiveDirectory module from the remote session
Import-PSSession -Session $dcSession -Module ActiveDirectory -Prefix Remote

# Now you can use AD commands as if they were local
Get-RemoteADUser -Filter *

# The imported commands are actually proxies that run remotely
Get-Command Get-RemoteADUser
(Get-Command Get-RemoteADUser).Definition

How it works:

  1. PowerShell connects to the remote machine
  2. Loads the requested module
  3. Gets the command definitions
  4. Creates local proxy functions with the same signatures
  5. When you call them, they execute remotely and return results

Scripting and Toolmaking

ISE Tips

The Integrated Scripting Environment (ISE) has useful shortcuts:

  • Ctrl+R: Switch between panes
  • Ctrl+J: Insert code snippets

Example: Get Free Disk Space

# Old approach
Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" | Select-Object FreeSpace

# Better: Convert to GB with rounding
Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" |
    Select-Object @{N='FreeGB'; E={$_.FreeSpace / 1GB -as [int]}}

# Modern approach using CIM
Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"

From Hard-Coded to Flexible

Step 1: Hard-coded (inflexible).

Get-WmiObject -ComputerName localhost -Class Win32_LogicalDisk -Filter "DeviceID='C:'"

Step 2: Variable (still requires editing).

$ComputerName = 'localhost'

Get-WmiObject -ComputerName $ComputerName -Class Win32_LogicalDisk -Filter "DeviceID='C:'" |
    Select-Object @{N="FreeGB"; E={$PSItem.FreeSpace / 1GB -as [int]}}

Step 3: Parameterized script.

param(
    [string[]]$ComputerName = 'localhost'
)

Get-WmiObject -ComputerName $ComputerName -Class Win32_LogicalDisk -Filter "DeviceID='C:'" |
    Select-Object @{N="FreeGB"; E={$PSItem.FreeSpace / 1GB -as [int]}}

Run it with: .\DiskInfo.ps1 -ComputerName Server01

Adding CmdletBinding

[CmdletBinding()] enables advanced parameter features:

[CmdletBinding()]
param(
    [Parameter(Mandatory=$True)]
    [string[]]$ComputerName,
    
    $Bogus
)

Get-WmiObject -ComputerName $ComputerName -Class Win32_LogicalDisk -Filter "DeviceID='C:'"

Adding Documentation

<#
.SYNOPSIS
    Gets free disk space from remote computers.

.DESCRIPTION
    Connects to specified computers and returns the free space
    on their C: drives in gigabytes.

.PARAMETER ComputerName
    One or more computer names to query.

.EXAMPLE
    .\DiskInfo.ps1 -ComputerName Server01
    Queries Server01 for free disk space.

.EXAMPLE
    .\DiskInfo.ps1 -ComputerName Server01, Server02
    Queries multiple servers.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$True)]
    [string[]]$ComputerName
)

Get-WmiObject -ComputerName $ComputerName -Class Win32_LogicalDisk -Filter "DeviceID='C:'"

Creating Functions

Wrap your code in a function following the Verb-Noun naming convention:

function Get-DiskInfo {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)]
        [string[]]$ComputerName
    )

    Get-WmiObject -ComputerName $ComputerName -Class Win32_LogicalDisk -Filter "DeviceID='C:'" |
        Select-Object @{N="FreeGB"; E={$PSItem.FreeSpace / 1GB -as [int]}}
}

# To use it, first source the script
. .\DiskInfo.ps1

# Then call the function
Get-DiskInfo -ComputerName localhost

Why the dot?

Running with . executes the script in the current scope, making the function available after the script exits. Without it, the function exists only while the script runs, then disappears.

Creating a Module

Quick method (requires importing each time):

  1. Rename your script from .ps1 to .psm1
  2. Import it: Import-Module .\DiskInfo.psm1 -Force -Verbose
  3. Use the function: Get-DiskInfo

Proper method (auto-loads):

  1. Create the folder: C:\Users\<Username>\Documents\WindowsPowerShell\Modules\DiskInfo
  2. Copy DiskInfo.psm1 into that folder
  3. PowerShell will automatically load it when you start a new session

Note: Never place custom modules in C:\Windows\System32\WindowsPowerShell\v1.0\Modules – that is reserved for Microsoft modules.