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 type | What it actually runs |
|---|---|
history | Get-History |
cd | Set-Location |
ls or dir | Get-ChildItem |
clear or cls | Clear-Host |
mkdir or md | New-Item -ItemType Directory |
man or help | Get-Help |
cat or type | Get-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:
Exportcommands convert data and write it to a file.ConvertTocommands only convert the data; you need to pipe it toOut-Fileyourself.
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:
- Each object coming through the pipeline gets assigned to
$_or$PSItem. - PowerShell evaluates the code inside
{}. - 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:
- Establish a TCP connection
- Start a PowerShell process on the remote machine
- Load the .NET Framework
- Initialize PowerShell
- Run profile scripts
- Send your code across the network
- Execute the code on the remote system
- Serialize the results into XML
- Transmit the XML back
- 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
.ps1files 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 allowedAllSigned- All scripts must be digitally signedRemoteSigned- Downloaded scripts must be signed; local scripts can runUnrestricted- 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:
- PowerShell connects to the remote machine
- Loads the requested module
- Gets the command definitions
- Creates local proxy functions with the same signatures
- 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 panesCtrl+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):
- Rename your script from
.ps1to.psm1 - Import it:
Import-Module .\DiskInfo.psm1 -Force -Verbose - Use the function:
Get-DiskInfo
Proper method (auto-loads):
- Create the folder:
C:\Users\<Username>\Documents\WindowsPowerShell\Modules\DiskInfo - Copy
DiskInfo.psm1into that folder - 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.