PowerShell โ Complete Beginner Tutorial
6 modules ยท 18 examples ยท Click โถ Run Script to see the console output
๐ก How to use this tutorial
Read each explanation, study the code, then click โถ Run Script to see the output.
Variables & Output
In PowerShell, every variable starts with a dollar sign ($). You do not need to declare a type โ PowerShell figures it out. Write-Host prints text to the console. String interpolation works inside double quotes โ variables are automatically replaced with their values. Single quotes treat everything literally. The $() syntax evaluates an expression inside a string.
Declaring Variables and Output
All PowerShell variables start with $. Assign with =. Use Write-Host to print to the screen. Double-quoted strings interpolate variables automatically. Use Write-Output to send data down the pipeline instead of just to the screen.
# Variables always start with $
$name = "Alice"
$age = 25
$city = "Cape Town"
$price = 29.99
$isAdmin = $true
# Write-Host โ print to screen
Write-Host "Hello, World!"
Write-Host "Name: $name"
# String interpolation in double quotes
Write-Host "Hello, $name! You are $age years old."
# $() โ evaluate an expression inside a string
Write-Host "Next year you will be $($age + 1)"
# Single quotes โ NO interpolation
Write-Host 'Literal: $name'
# Multiple values at once
Write-Host "City: $city, Price: $price, Admin: $isAdmin"The key difference: double quotes "Hello $name" interpolate variables. Single quotes 'Hello $name' are literal. This is the same as in Bash. Use single quotes when you want the $ to be treated as a real dollar sign character and not a variable.
Data Types and Type Checking
PowerShell is dynamically typed but works with .NET types under the hood. You can check a variable's type with .GetType(). Cast to another type by putting [TypeName] before a value. The common types are [string], [int], [double], [bool], [datetime], and [array].
# PowerShell infers types automatically
$text = "Hello PowerShell"
$number = 42
$decimal = 3.14
$flag = $true
$nothing = $null
# Check the .NET type of any variable
Write-Host $text.GetType().Name # String
Write-Host $number.GetType().Name # Int32
Write-Host $decimal.GetType().Name # Double
Write-Host $flag.GetType().Name # Boolean
# Type casting
$numStr = [string]42 # 42 as string "42"
$intStr = [int]"99" # "99" as integer 99
$dbl = [double]"3.14"
Write-Host "Cast to string: $numStr"
Write-Host "Cast to int: $intStr"
# Arithmetic
$a = 10
$b = 3
Write-Host "$a + $b = $($a + $b)"
Write-Host "$a * $b = $($a * $b)"
Write-Host "$a / $b = $($a / $b)"
Write-Host "$a % $b = $($a % $b)"PowerShell uses .NET types internally. Int32 is a 32-bit integer. You can call any .NET method on PowerShell values โ for example $text.ToUpper(), $text.Length, $text.Replace("a","b"). This is what makes PowerShell more powerful than Bash for complex data manipulation.
String Manipulation
Strings in PowerShell are .NET String objects so they have dozens of built-in methods. The -f format operator lets you format strings like printf in C. Common string methods: ToUpper(), ToLower(), Trim(), Replace(), Split(), Substring(), StartsWith(), Contains().
$text = " Hello, PowerShell World! "
# Case
Write-Host $text.ToUpper()
Write-Host $text.ToLower()
# Trim whitespace
Write-Host $text.Trim()
# Length
Write-Host "Length: $($text.Length)"
# Replace
Write-Host $text.Replace("PowerShell", "PS")
# Contains / StartsWith
Write-Host $text.Contains("World") # True
Write-Host $text.StartsWith(" He") # True
# Split into array
$words = "one,two,three,four".Split(",")
Write-Host $words.Count # 4
Write-Host $words[0] # one
# -f format operator
$name = "Alice"
$score = 92.5
Write-Host ("Name: {0,-10} Score: {1:F2}" -f $name, $score)The -f format operator is very useful for aligned output in tables and reports. {0} is the first argument, {1} is the second. {0,-10} left-aligns in 10 characters. {1:F2} formats a number to 2 decimal places. {2:D5} pads an integer to 5 digits. This is identical to .NET String.Format().
Control Flow
PowerShell uses -eq, -ne, -gt, -lt, -ge, -le for comparisons (not ==, !=, >, < like most languages). This is because > and < are used for redirection in the shell. Logical operators are -and, -or, -not. The if statement, switch, and ternary-style expressions all work similarly to other languages but with these different operators.
if / elseif / else
PowerShell comparison operators: -eq (equal), -ne (not equal), -gt (greater than), -lt (less than), -ge (greater or equal), -le (less or equal). For strings: -like uses wildcards (*), -match uses regex, -contains checks if a collection has a value. Note: comparisons are case-insensitive by default.
$score = 82
# if / elseif / else
if ($score -ge 90) {
Write-Host "Grade: A โ Distinction"
} elseif ($score -ge 75) {
Write-Host "Grade: B โ Merit"
} elseif ($score -ge 60) {
Write-Host "Grade: C โ Pass"
} else {
Write-Host "Grade: F โ Try again"
}
# Logical operators: -and -or -not
$age = 20
$hasTicket = $true
if ($age -ge 18 -and $hasTicket) {
Write-Host "Entry allowed"
}
# String comparison โ case insensitive by default
$city = "Cape Town"
if ($city -eq "cape town") {
Write-Host "Matched: $city"
}
# -like โ wildcard matching
if ($city -like "Cape*") {
Write-Host "Starts with Cape"
}
# One-liner if (no braces for single statement)
if ($score -gt 50) { Write-Host "You passed!" }PowerShell comparisons are case-insensitive by default. "hello" -eq "HELLO" is True. To make them case-sensitive, prefix with c: "hello" -ceq "HELLO" is False. Similarly -like vs -clike, -match vs -cmatch. This is different from most languages which are always case-sensitive.
switch Statement
PowerShell switch is more powerful than a typical switch โ it can match wildcards (-Wildcard), regex patterns (-Regex), and runs ALL matching cases unless you use break. By default it is case-insensitive. It also accepts an array as the test value and evaluates every element against the cases.
# Basic switch
$day = "Wednesday"
switch ($day) {
"Monday" { Write-Host "Start of the week" }
"Tuesday" { Write-Host "Second day" }
"Wednesday" { Write-Host "Midweek!" }
"Thursday" { Write-Host "Almost Friday" }
"Friday" { Write-Host "TGIF!" }
default { Write-Host "Weekend!" }
}
# switch with -Wildcard
$filename = "report_2025.xlsx"
switch -Wildcard ($filename) {
"*.xlsx" { Write-Host "Excel file" }
"*.csv" { Write-Host "CSV file" }
"report*" { Write-Host "It is a report" }
default { Write-Host "Unknown type" }
}
# switch with ranges (using conditions)
$temp = 28
$weather = switch ($true) {
{ $temp -lt 0 } { "Freezing"; break }
{ $temp -lt 15 } { "Cold"; break }
{ $temp -lt 25 } { "Mild"; break }
{ $temp -lt 35 } { "Warm"; break }
default { "Hot" }
}
Write-Host "Weather: $weather"PowerShell switch runs ALL matching cases by default โ unlike switch in C, Java or JavaScript which stops at the first match. If report_2025.xlsx matches both "*.xlsx" and "report*", both blocks run. Add break inside each case if you want to stop after the first match. This is a very common gotcha.
Loops
PowerShell has four loop types: foreach (for iterating collections), for (C-style counter loop), while (condition-based), and do-while (runs at least once). The most PowerShell-idiomatic way to loop over a collection is with ForEach-Object in the pipeline, which processes each item piped to it. break exits a loop. continue skips to the next iteration.
foreach and for Loops
foreach iterates over every item in a collection. The variable you name (like $fruit) holds the current item each time through. The C-style for loop has three parts: initialiser, condition, increment โ all separated by semicolons. Both support break and continue.
# foreach loop
$fruits = @("apple", "banana", "cherry", "mango")
foreach ($fruit in $fruits) {
Write-Host "- $fruit"
}
Write-Host "---"
# foreach with index (use a counter variable)
$i = 0
foreach ($fruit in $fruits) {
Write-Host "$($i + 1). $fruit"
$i++
}
Write-Host "---"
# C-style for loop
for ($n = 1; $n -le 5; $n++) {
Write-Host "Count: $n"
}
Write-Host "---"
# 1..10 range operator
foreach ($num in 1..5) {
Write-Host -NoNewline "$num "
}
Write-Host ""The .. range operator (1..10) creates an array of integers. It works in both directions: 10..1 counts down. You can use it directly in foreach, or with ForEach-Object in the pipeline: 1..10 | ForEach-Object { $_ * 2 }. This is a very clean PowerShell pattern for generating sequences.
while Loop and Pipeline ForEach-Object
while runs as long as the condition is true. do-while runs at least once then checks. ForEach-Object (alias: %) is the pipeline version โ it processes each object piped to it. The $_ automatic variable holds the current pipeline object. This is the most idiomatic PowerShell way to iterate.
# while loop
$count = 1
while ($count -le 5) {
Write-Host "Count: $count"
$count++
}
Write-Host "---"
# do-while โ runs at least once
$num = 1
do {
Write-Host "do-while: $num"
$num++
} while ($num -le 3)
Write-Host "---"
# ForEach-Object in pipeline โ $_ is current item
1..5 | ForEach-Object {
Write-Host "Square of $_: $($_ * $_)"
}
Write-Host "---"
# break and continue
Write-Host "Skip 3, stop at 7:"
foreach ($i in 1..10) {
if ($i -eq 3) { continue } # skip 3
if ($i -eq 7) { break } # stop at 7
Write-Host -NoNewline "$i "
}
Write-Host ""$_ (dollar underscore) is the automatic pipeline variable โ it holds the current object being processed in ForEach-Object, Where-Object, and other pipeline cmdlets. Some scripts also use $PSItem which is an alias for $_. You will see $_ everywhere in PowerShell scripts.
Functions
PowerShell functions follow the Verb-Noun naming convention (Get-Greeting, Set-Config, New-Report). Parameters are declared in a param() block at the top of the function. Parameters can be mandatory, have defaults, and accept pipeline input. Functions implicitly return the last value in the function body โ anything written to the output stream is returned. Use return to exit early.
Defining and Calling Functions
Use the function keyword then the name. Follow Verb-Noun naming. Parameters go in a param() block with optional default values. PowerShell functions implicitly return all output โ everything you write to the pipeline inside the function becomes the return value.
# Simple function
function Say-Hello {
Write-Host "Hello from PowerShell!"
}
Say-Hello
# Function with parameters
function Get-Greeting {
param(
[string]$Name,
[string]$Greeting = "Hello"
)
"$Greeting, $Name!"
}
Write-Host (Get-Greeting -Name "Alice")
Write-Host (Get-Greeting -Name "Bob" -Greeting "Good morning")
# Function that returns a value
function Get-BMI {
param(
[double]$WeightKg,
[double]$HeightM
)
[math]::Round($WeightKg / ($HeightM * $HeightM), 1)
}
$bmi = Get-BMI -WeightKg 70 -HeightM 1.75
Write-Host "BMI: $bmi"
# Function returning grade
function Get-Grade {
param([int]$Score)
if ($Score -ge 90) { return "A" }
if ($Score -ge 75) { return "B" }
if ($Score -ge 60) { return "C" }
"F"
}
Write-Host "Score 88 = $(Get-Grade 88)"
Write-Host "Score 55 = $(Get-Grade 55)"PowerShell uses named parameters (-Name "Alice") not positional by default. You can call Get-Greeting "Alice" without the -Name if it is the first positional parameter, but using names makes scripts self-documenting and order-independent. Always use named parameters in scripts that others will read.
Advanced Functions with [CmdletBinding]
Adding [CmdletBinding()] to a function makes it a proper "advanced function" with the same features as built-in cmdlets โ including -Verbose, -ErrorAction, and pipeline input support. The [Parameter(Mandatory)] attribute forces the caller to provide the value. [ValidateRange] and [ValidateSet] validate input automatically.
function Get-DiskReport {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ComputerName,
[ValidateRange(1, 100)]
[int]$ThresholdPercent = 80,
[ValidateSet("GB", "MB", "TB")]
[string]$Unit = "GB"
)
Write-Verbose "Checking $ComputerName (threshold: $ThresholdPercent%)"
# Simulate disk data
$disks = @(
[PSCustomObject]@{ Drive = "C:"; UsedGB = 85; TotalGB = 100 }
[PSCustomObject]@{ Drive = "D:"; UsedGB = 200; TotalGB = 500 }
)
foreach ($disk in $disks) {
$pct = [math]::Round(($disk.UsedGB / $disk.TotalGB) * 100)
$status = if ($pct -ge $ThresholdPercent) { "WARNING" } else { "OK" }
Write-Host "[$status] $($disk.Drive) $pct% used"
}
}
Get-DiskReport -ComputerName "Server01"
Get-DiskReport -ComputerName "Server02" -ThresholdPercent 50Write-Verbose "message" only shows when the caller passes -Verbose. This is the professional way to add debug/progress output to functions without always cluttering the output. Advanced functions also get -Debug, -ErrorAction, -WhatIf and -Confirm for free โ the same parameters all built-in cmdlets support.
Arrays & Hashtables
Arrays in PowerShell are created with @() or by separating values with commas. They are zero-indexed. Hashtables (key-value pairs) are created with @{}. PSCustomObject lets you build structured objects on the fly. The pipeline works seamlessly with arrays โ you can pipe an array through Where-Object to filter and ForEach-Object to transform.
Arrays โ Creating and Using
Create arrays with @() or comma-separated values. Access elements with [index] โ zero-based. Use .Count for the length. Add elements with += (creates a new array). Filter with Where-Object. Transform with ForEach-Object. Sort with Sort-Object.
# Creating arrays
$fruits = @("apple", "banana", "cherry", "mango")
$numbers = @(3, 1, 4, 1, 5, 9, 2, 6)
# Accessing elements โ zero-indexed
Write-Host "First: $($fruits[0])"
Write-Host "Last: $($fruits[-1])"
Write-Host "Count: $($fruits.Count)"
# Add an element (creates new array)
$fruits += "grape"
Write-Host "After add: $($fruits.Count) fruits"
# Filter with Where-Object
$big = $numbers | Where-Object { $_ -gt 4 }
Write-Host "Over 4: $big"
# Transform with ForEach-Object
$doubled = $numbers | ForEach-Object { $_ * 2 }
Write-Host "Doubled: $doubled"
# Sort
$sorted = $numbers | Sort-Object
Write-Host "Sorted: $sorted"
# Contains check
Write-Host $fruits.Contains("banana") # True
# Join into string
Write-Host ($fruits -join ", ")$numbers | Where-Object { $_ -gt 4 } is the PowerShell equivalent of Python's filter() or JavaScript's .filter(). The $_ refers to the current element being tested. You can also use the shorter alias: Where-Object { $_ -gt 4 } can be written as ? { $_ -gt 4 } in PowerShell.
Hashtables and PSCustomObject
Hashtables @{} store key-value pairs โ like dictionaries in Python or objects in JavaScript. Access values with .Key or ["Key"]. PSCustomObject creates a lightweight object with named properties โ perfect for structured data you want to output as a table.
# Hashtable โ key-value pairs
$person = @{
Name = "Alice"
Age = 25
City = "Cape Town"
Admin = $true
}
# Access values
Write-Host "Name: $($person.Name)"
Write-Host "Age: $($person["Age"])"
# Add and update
$person["Email"] = "alice@example.com"
$person.Age = 26
# Check if key exists
Write-Host $person.ContainsKey("Email") # True
# Loop over hashtable
foreach ($key in $person.Keys) {
Write-Host " $key: $($person[$key])"
}
# PSCustomObject โ structured named object
$user = [PSCustomObject]@{
Name = "Bob"
Score = 92
Grade = "A"
}
Write-Host "---"
Write-Host "$($user.Name) scored $($user.Score) โ Grade $($user.Grade)"
# Array of PSCustomObjects โ auto-formats as table
$students = @(
[PSCustomObject]@{ Name = "Alice"; Score = 92 }
[PSCustomObject]@{ Name = "Bob"; Score = 78 }
[PSCustomObject]@{ Name = "Carol"; Score = 95 }
)
$students | Sort-Object Score -Descending | Format-TablePSCustomObject is incredibly useful for creating structured output. When you output an array of PSCustomObjects, PowerShell automatically formats them as a table with column headers. This is how professional PowerShell scripts produce clean, readable output โ much better than concatenating strings.
Pipeline & File Operations
The PowerShell pipeline (|) is its most distinctive and powerful feature. Unlike Bash which passes text, PowerShell passes .NET objects โ so the receiving command gets structured data. Get-Process returns process objects with CPU, Memory, Name properties. You can filter with Where-Object, sort with Sort-Object, select columns with Select-Object, and group with Group-Object. File operations use Get-Content, Set-Content, Add-Content, Copy-Item, Move-Item, Remove-Item, and New-Item.
The Object Pipeline
Chain commands with | to pass objects between them. Where-Object (alias: ?) filters. Select-Object (alias: select) picks properties. Sort-Object sorts. Measure-Object calculates statistics. ForEach-Object (alias: %) transforms. The $_ variable holds the current object in the pipeline.
# Pipeline โ chain of object processing
# Get-Process returns real running process objects
# (Shown with simulated data)
# Simulate some process objects
$processes = @(
[PSCustomObject]@{ Name = "chrome"; CPU = 245.3; Memory = 512 }
[PSCustomObject]@{ Name = "code"; CPU = 42.6; Memory = 320 }
[PSCustomObject]@{ Name = "node"; CPU = 87.1; Memory = 180 }
[PSCustomObject]@{ Name = "pwsh"; CPU = 5.2; Memory = 64 }
[PSCustomObject]@{ Name = "msedge"; CPU = 150.0; Memory = 400 }
)
# Filter โ only CPU > 50
$heavy = $processes | Where-Object { $_.CPU -gt 50 }
Write-Host "High CPU processes:"
$heavy | ForEach-Object { Write-Host " $($_.Name): $($_.CPU)" }
Write-Host "---"
# Sort descending
$top3 = $processes | Sort-Object CPU -Descending | Select-Object -First 3
Write-Host "Top 3 by CPU:"
$top3 | ForEach-Object { Write-Host " $($_.Name): $($_.CPU)" }
Write-Host "---"
# Measure โ statistics
$stats = $processes | Measure-Object -Property CPU -Sum -Average -Max
Write-Host "Total CPU: $($stats.Sum)"
Write-Host "Avg CPU: $([math]::Round($stats.Average, 1))"
Write-Host "Max CPU: $($stats.Maximum)"Select-Object -First 3 takes only the first 3 objects โ like LIMIT in SQL or [:3] in Python. Select-Object -Last 5 takes the last 5. Select-Object Name, CPU picks only those two properties from each object. This lets you slice data any way you need without writing loops.
Working with Files and Folders
PowerShell file cmdlets follow the Verb-Noun convention. Get-Content reads a file line by line. Set-Content writes (overwrites). Add-Content appends. New-Item creates files or folders. Copy-Item, Move-Item, Remove-Item do what their names suggest. Test-Path checks if something exists.
# Create a folder
New-Item -Path "C:\Reports" -ItemType Directory -Force
# Create and write a file
$content = "Line 1: PowerShell is powerful`nLine 2: Objects everywhere`nLine 3: Done"
Set-Content -Path "C:\Reports\output.txt" -Value $content
# Read the file
$lines = Get-Content -Path "C:\Reports\output.txt"
Write-Host "File has $($lines.Count) lines"
foreach ($line in $lines) {
Write-Host " >> $line"
}
# Append to file
Add-Content -Path "C:\Reports\output.txt" -Value "Line 4: Appended"
# Check if path exists
if (Test-Path "C:\Reports\output.txt") {
Write-Host "File exists"
}
# List files in a folder
$files = Get-ChildItem -Path "C:\Reports" -Filter "*.txt"
Write-Host "TXT files found: $($files.Count)"
# Copy and rename
Copy-Item "C:\Reports\output.txt" "C:\Reports\backup.txt"
# Remove a file
Remove-Item "C:\Reports\backup.txt"
Write-Host "Backup removed"Test-Path is one of the most important cmdlets for robust scripts. Always check if a path exists before trying to use it โ this prevents errors when files or folders are missing. Use if (Test-Path $path) { ... } before Get-Content, Remove-Item, or any operation that requires the path to exist.
You finished the PowerShell tutorial!
You can now write PowerShell scripts with variables, control flow, loops, functions, arrays, hashtables and file operations. You have the skills to automate Windows tasks and start learning Azure and Active Directory automation.