In this Knowledge Base (KB) article, we will guide you through the process of writing a PowerShell script, specifically focusing on creating a function. This function will help you automate a repeatable task, saving you time and effort.
But first, some basics.
What is Windows PowerShell
Windows PowerShell is a task automation framework and a command-line shell developed by Microsoft.
Windows PowerShell is built on top of the .NET Framework and supports cmdlets (pronounced “command-lets”), which are small, focused scripts or tools that perform a specific task. These cmdlets can be combined and reused to create more complex PowerShell scripts and automation workflows.
What is a PowerShell script?
PowerShell scripts are files that contain a series of PowerShell commands intended for scenarios like the automation of administrative tasks, configuration management and data processing.
Tools used
While tool selection is largely a matter of personal preference, I prefer using the following tools for writing, testing, and running scripts:
Visual Studio Code – Visual Studio Code – Code Editing. Redefined
The PowerShell plugin for Visual Studio Code – PowerShell – Visual Studio Marketplace
Windows Terminal – An overview on Windows Terminal | Microsoft Learn
Components of a PowerShell script
Cmdlets
PowerShell cmdlets, or commandlets, are specialized commands created for system administration and automation tasks. They tend to follow a naming convention of verb-noun (e.g., Get-Process or New-Item). Cmdlets perform very specific operations, and their output is generally structured as objects, allowing them to be easily combined using pipelines to chain multiple commands together.
Examples include Get-Process, Set-Location, Get-Help, Start-Service, Remove-Item and Get-WmiObject
# Example PowerShell script using Get-Process
# Retrieve all running processes
$processes = Get-Process
# Filter processes that are consuming more than 100 MB of memory
$highMemoryProcesses = $processes | Where-Object { $_.WorkingSet -gt 100MB }
# Export the filtered list of processes to a CSV file
$highMemoryProcesses | Export-Csv -Path "C:TempHighMemoryProcesses.csv" -NoTypeInformation
# Output the count of high memory processes to the console
$processCount = $highMemoryProcesses.Count
Write-Output "Number of processes consuming more than 100 MB of memory: $processCount"
# Output message indicating the location of the exported CSV file
Write-Output "Filtered process list exported to C:TempHighMemoryProcesses.csv"
Admins will generally use Cmdlets to perform ad hoc tasks, such as managing Active Directory users, Exchange mailboxes, Azure resources, Endpoint devices, etc.
As an example, here is the Cmdlet Get-ADUser in use.
# Import the Active Directory module if not already imported
Import-Module ActiveDirectory
# Get a specific user by their username
$user = Get-ADUser -Identity "jdoe"
# Display the user details
Write-Output $user
DistinguishedName : CN=John Doe,OU=Users,DC=example,DC=com
Enabled : True
GivenName : John
Name : John Doe
ObjectClass : user
ObjectGUID : 12345678-90ab-cdef-1234-567890abcdef
SamAccountName : jdoe
SID : S-1-5-21-1234567890-123456789-1234567890-1234
Surname : Doe
UserPrincipalName : [email protected]
You can find more examples of common PowerShell uses here
Pipelines
We can take Cmdlets and supercharge them by combining them with other Cmdlets through the use of the Pipeline.
A pipeline is basically a series of commands connected together by the pipeline operator ( | ). Each time you add a pipeline, the results of the preceding command are forwarded to the next.
As an example, let’s get all processes using Get-Process, assign the results to $highMemoryProcesses, pipe that to Where-Object to find processes consuming over 100MB of memory, sort by WorkingSet, and take the top 5 highest results.
$highMemoryProcesses = Get-Process | Where-Object { $_.WorkingSet -gt 100MB }
$highMemoryProcesses | Sort-Object -Property WorkingSet -Descending | Select-Object -First 5
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
0 3.62 557.01 0.00 5004 0 Memory Compression
53 726.05 533.55 145.42 9056 2 msedgewebview2
195 374.01 507.14 14.14 39864 2 explorer
262 396.18 487.10 19.59 39876 2 RemoteDesktopManager
208 304.77 470.62 49.39 14872 2 msedge
As another example, we can use Get-ChildItem to get the contents of a directory and pipe the results directly to Sort-Object to sort by the file length property.
Get-ChildItem -Path "C:devHydrationKitWS2022" -File | Sort-Object -Property Length -Descending
Directory: C:devHydrationKitWS2022
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 03/07/2024 11:12 31071 README.md
-a--- 03/07/2024 11:12 15114 CustomizeHydrationKit.ps1
-a--- 03/07/2024 11:12 7547 New-HydrationKitSetup.ps1
-a--- 03/07/2024 11:12 4514 New-LabVMsForHyperV.ps1
-a--- 03/07/2024 20:29 1857 Export-WindowsServer2022WIMfromISO.ps1
-a--- 03/07/2024 11:29 1779 Export-Windows11EnterpriseWIMFromISO.ps1
-a--- 03/07/2024 11:12 1096 LICENSE
-a--- 03/07/2024 11:12 698 Update-MDTMediaCopyToHost.ps1
Variables:
Variables allow you to store all types of values, such as the output of a pipeline operation or data you want to use in other cmdlets, such as file paths or values.
There are a few different types of variables: user-defined, automatically defined, and preference variables.
User-defined variables
# Declaring a string variable
$message = "Hello, World!"
Write-Output $message # Output: Hello, World!
# Declaring an array variable
$numbers = @(1, 2, 3, 4, 5)
Write-Output $numbers # Output: 1 2 3 4 5
# Accessing array elements
Write-Output $numbers[0] # Output: 1
Write-Output $numbers[2] # Output: 3
# Declaring a hash table variable
$person = @{
Name = "John Doe"
Age = 30
Country = "USA"
}
Write-Output $person # Output: @{Name=John Doe; Age=30; Country=USA}
# $PSVersionTable: Contains information about the PowerShell version
Write-Output $PSVersionTable
# Declaring a global variable
$global:globalVariable = "I am global"
Write-Output $global:globalVariable # Output: I am global
# Declaring a local variable
$local:localVariable = "I am local"
Write-Output $local:localVariable # Output: I am local
# Arithmetic operations
$a = 10
$b = 20
$sum = $a + $b
Write-Output $sum # Output: 30
# String concatenation
$firstName = "John"
$lastName = "Doe"
$fullName = $firstName + " " + $lastName
Write-Output $fullName # Output: John Doe
# Using variables in strings
$greeting = "Hello, $firstName $lastName!"
Write-Output $greeting # Output: Hello, John Doe!
Automatically defined variables
$PSVersionTable contains details about the PowerShell version and environment
$PSVersionTable
Name Value
---- -----
PSVersion 7.1.3
PSEdition Core
GitCommitId 7.1.3
OS Microsoft Windows 10.0.19041
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
$HOME contains the full path of the user’s home directory
$HOME
C:UsersUsername
$Profile contains the full path to the user’s profile script
$PROFILE
C:UsersUsernameDocumentsPowerShellMicrosoft.PowerShell_profile.ps1
Preference variables
As an example $ErrorActionPreference lets you instruct PowerShell what to do when there is a non-terminating error (an error that does not stop a cmdlet from executing)
Possible values:
Continue (default) – Displays the error and continues execution.
Stop – Stops execution.
SilentlyContinue – Suppresses the error message and continues execution.
Inquire – Asks for user input on how to proceed.
Ignore – Ignores the error completely (PowerShell 7+).
Flow Control
When you start to move away from using single cmdlets, one line scripts and combine everything together into a script, it may sound scarier than it actually is. Very simply, a script is just the same commands you’re already familiar with, bundled together and saved in a .PS1.
These controls include If, Else, ForEach, For, Do(including While and Until), While, Break, Continue and Return.
# Example of an if statement
$number = 10
if ($number -gt 5) {
Write-Output "The number is greater than 5."
} elseif ($number -eq 5) {
Write-Output "The number is equal to 5."
} else {
Write-Output "The number is less than 5."
}
# Using foreach loop
$array = 1..5
foreach ($item in $array) {
if ($item -eq 3) {
Write-Output "Skipping item $item using continue."
continue
}
Write-Output "Processing item $item in foreach loop."
}
# Using while loop
$counter = 0
while ($counter -lt 5) {
if ($counter -eq 3) {
Write-Output "Breaking out of the while loop at counter $counter."
break
}
Write-Output "Processing counter $counter in while loop."
$counter++
}
# Using for loop
for ($i = 0; $i -lt $array.Length; $i++) {
if ($i -eq 2) {
Write-Output "Breaking out of the for loop at index $i."
break
}
Write-Output "Processing index $i in for loop."
}
Functions
The last piece of our PowerShell script puzzle is functions. With a function, you can take one-line cmdlets you regularly use, modify them, and make them more easily reusable. Writing functions lets you reuse your code across multiple scripts instead of rewriting the same thing repeatedly or even save them as modules, which enables you to call them in PowerShell without referencing them directly by their full path.
Two critical things for a function are how to name it and its parameters, so let’s look at those things!
Naming
Naming Functions is important! They should follow the PascalCase style format and use an approved verb followed by a singular noun, e.g., Get-Version, Convert-Image, or Test-Binding. This is especially helpful for other people who may come across your function; a well-named function can immediately tell someone exactly what it does.
You can find a full list of approved verbs by using Get-Verb.
Parameters
Parameters let you expand your function, making it more fluid than static.
If we look at another simple Hello World example, This function will write out “Hello, World!” each time it is called.
function Say-Hello {
Write-Output "Hello, World!"
}
Say-Hello
Hello, World!
But if we expand the function to include a string parameter, we can have it do more! By adding a Name as a parameter, we can instruct the function to add a name to the output.
function Say-Hello {
param (
[string]$Name
)
Write-Output "Hello, $Name!"
}
# To call the function with a parameter
Say-Hello -Name "Alice"
Hello, Alice!
Lastly, if we combine our parameter with an if statement, we can expand the functionality further to change the output depending on whether or not the parameter was used.
function Say-Hello {
param (
[string]$Name
)
if ($name){
Write-Output "Hello, $Name!"
} else {
Write-Output "Hello, World!"
}
}
You can also easily transform your simple function into an advanced function with CmdletBiding, SupportShouldProcess, Validation, Pipeline input, and Error Handling, but we’ll look at those in some examples later.
Formatting
I won’t reinvent the wheel here, rules and recommendations for formatting PowerShell scripts are fairly standard and widely accepted, So lets take some advise from Don Jones, Matt Penny, Carlos Perez, Joel Bennett and the PowerShell Community.
Let’s look at what I believe to be the three most essential things in formatting.
Consistency
While the rules may differ between projects, the goal is to format code consistently. Always aim to stick to the style guidelines laid out in a project, or pick a style for your own projects and stick with it.
Capitalization
PowerShell isn’t case-sensitive, but following capitalization conventions makes code easier to read. Microsoft created these conventions for .NET, and as PowerShell is based on .NET, they all. You can read more about these conventions here – Capitalization Conventions – Framework Design Guidelines | Microsoft Learn, but tl;dr
lowercase – all lowercase, no word separation
Keywords like foreach are written in lower case, as well as operators like -eq and -match
UPPERCASE – all capitals, no word separation
Keywords in comment-based help are written in upper case for readability
PascalCase – capitalize the first letter of each word
PascalCase is used for all public identifiers.
camelCase – capitalize the first letter of each word except the first.
camelCase is really a matter of personal preference.
Indentation
Contrary to the style guide, Tabs not spaces. Going back to consistency, if you have a preference then stick with it or stick with the style of the project.
Other things to think about
Our first PowerShell script
Before you can start writing PowerShell scripts, you really need to know what you want to do or what problem you are trying to solve. Let’s take a problem I encountered recently.
Converting images
Problem
I encountered an issue where downloading images from the internet that had a file extension of .PNG were actually .JPG, and while that may not seem like that big of an issue, it can cause problems with app packaging for Intune.
Solution
Go to https://cloudconvert.com/, right? No. Do it in PowerShell!
If we break it down, I want to
Take in an image path
Take in an optional output path
Take in a required output image type (jpg, jpeg, png, gif, bmp or tiff)
Import the image to a variable
Check if an output path has been specified
If not, generate one
Convert the image to the selected format
Save it to the output path with the new file type
Write out success or failure to the console
Getting started
The first thing we want to do is take in an image path, an optional output path, and a required output image type. Let’s set up our CmdletBinding and parameters.
Firstly, we’ve added a CmdletBinding to add Validation and Error Handling.
Now, let’s add a mandatory parameter called $InputImagePath to specify which image to pull in.
Then, we add a mandatory parameter called $OutputFormat to let us specify which format we want to convert our image to. We’ve also added validation to ensure that the output format is an accepted format.
Lastly, we’ve added a parameter called $OutputImagePath so that we can choose to specify an output path, but we don’t have to as it isn’t mandatory.
Now that we have everything we need to take in, Lets figure out processing.
Validation
Before we go any further, we want to ensure that the $InputImagePath is provided.
We can test the $InputImagePath using Test-Path, and if it doesn’t exist, we’ll throw an error message. Throw will stop the execution of the script unless there is a try/catch block, which we’ll come to shortly.
Working with directories
Now, we need to do a little more validation to ensure that our output directory exists so that we can save our converted image later.
We’ll do this in 2 steps
Check if our $OutputImagePath variable was passed
If it wasn’t, set the $OutputImagePath to the same directory as $InputImagePath.
If $OutputImagePath was specified, validate that it exists and is a valid path.
If it is not a valid path, throw an error
If it is a valid path, set it as our $OutputImagePath
Now that we have our $OutputImagePath set, we can start to work with our images.
Working with images
Now that we’ve added some validation, we now need to start working with images. We can do this by using Add-Type to load in the System.Drawing assembly which gives our script access to classes and methods used to manipulate images.
Once System.Drawingis imported, we can load our image from $InputImagePath, using the FromFile() method, into PowerShell to start manipulating it.
With our image stored, we now need to set the output format, which we’ll take from the required $OutputFormat parameter.
I’ve done this using a switch so we can see what is happening with the format variable, but we can shorten this to one line, as shown below.
Here, I have condensed this to one line that takes $OutputFormat and sets it as our output file format.
We can save our image once we have our $OutputImagePath and format!
Does it work?
Here, we can see two things.
There is only one image file, called ThisIsA.png.
There are a several ways to run PowerShell scripts, but we’ll dot source this one so that we can call our function in the scope of my terminal.
When we run our function, we pass in the required $InputImagePath and $OutputFormat parameters, and we can now see that we have a second file called ThisIsA.jpeg
That’s us all done, right? Well, it could be, but let’s take it just one step further and look at error handling.
Error handling
While it may not seem like a big deal for a simple small function, but it multiple small functions in a large script with no error handling can make troubleshooting difficult.
I’ve added a try/catch block around the Process section of my function, which lets me catch and handle different types of error. You don’t necessarily need to be overly granular with your catches, but if you are, you can perform different actions depending on the type of exception. In my scenario, I’m just going to write out a different error depending on the exception thrown.
A try/catch is not complete without a finally. While this won’t always be required, it’s generally good practice to release the resources held by the object you were working with, in this case, our $image.
So, after our catches, I’ve added a check to see if our image variable is null. If it isn’t, we’ll dispose of it to release the resources it was using to store our image.
The full script
I’ve also added a comment-based help section to the top of our script, including a description, parameter details, examples, and notes.
Note: You’ll notice that I’ve used a mixture of PascalCase and camelCase in my script. I like to do this to differentiate Parameters (PascaleCase) from variables (camelCase), but this is purely a personal preference.
<#
.SYNOPSIS
Converts the format of an image file to a specified format.
.DESCRIPTION
The Convert-ImageFormat function converts an image from one format to another. It requires the path to the input image and the desired output format. Optionally, a path for the output image can be specified; otherwise, the output will be saved in the same location as the input image with the new format.
.PARAMETER InputImagePath
The path to the input image file that needs to be converted.
.PARAMETER OutputFormat
The desired format for the output image. Valid options are "jpg", "jpeg", "png", "gif", "bmp", and "tiff".
.PARAMETER OutputImagePath
(Optional) The path where the converted image will be saved. If not specified, the image will be saved in the same location as the input file with the new format.
.EXAMPLE
Convert-ImageFormat -InputImagePath "C:pathtoinputimage.png" -OutputFormat "jpg"
This command converts an image named "image.png" to a JPG format.
.EXAMPLE
Convert-ImageFormat -InputImagePath "C:pathtoinputimage.png" -OutputFormat "gif" -OutputImagePath "C:pathtooutputimage.gif"
This command converts an image named "image.png" to a GIF format and saves it as "image.gif" in the specified path.
.NOTES
Author: Scott McAlliister
Version: 1.0
#>
function Convert-ImageFormat {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$InputImagePath,
[Parameter(Mandatory=$true)]
[ValidateSet("jpg", "jpeg", "png", "gif", "bmp", "tiff")]
[string]$OutputFormat,
[Parameter(Mandatory=$false)]
[string]$OutputImagePath
)
try {
if (-not (Test-Path -Path $InputImagePath -PathType Leaf)) {
throw "Input image path does not exist: $InputImagePath"
}
if (-not $OutputImagePath) {
$fileDirectory = [System.IO.Path]::GetDirectoryName($InputImagePath)
$fileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($InputImagePath)
$OutputImagePath = Join-Path -Path $fileDirectory -ChildPath "$fileNameWithoutExtension.$OutputFormat"
} else {
if (-not (Test-Path -Path $OutputImagePath -PathType Container)) {
throw "Output image path is not a valid directory: $OutputImagePath"
}
$fileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($InputImagePath)
$OutputImagePath = Join-Path -Path $OutputImagePath -ChildPath "$fileNameWithoutExtension.$OutputFormat"
}
Add-Type -AssemblyName System.Drawing
$image = [System.Drawing.Image]::FromFile($InputImagePath)
$format = [System.Drawing.Imaging.ImageFormat]::$OutputFormat
$image.Save($OutputImagePath, $format)
} catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {
Write-Error "File or directory not found: $_"
} catch [System.ArgumentException] {
Write-Error "An argument provided to a method was not valid: $_"
} catch [System.Runtime.InteropServices.ExternalException] {
Write-Error "An error occurred in the external GDI+ graphics interface: $_"
} catch [System.OutOfMemoryException] {
Write-Error "Not enough memory to continue the execution of the program: $_"
} catch {
Write-Error "An unexpected error occurred: $_"
} finally {
if ($image -ne $null) {
$image.Dispose()
}
}
}
Finishing up
This article is in no way the definitive guide to writing PowerShell scripts. You can write your scripts however you want, as long as they are readable, consistent, and formatted.
While this blog focussed specifically on a PowerShell script that contained a single function, a PowerShell script can be made of up many functions, single line commands or even call other scripts!
Recommended reading, resources, and communities
I recommend checking out a few resources if you’re interested in improving your PowerShell.
Learn PowerShell in a Month of Lunches, Fourth Edition (manning.com) – A fantastic book to get you started with all things PowerShell
Learn PowerShell Scripting in a Month of Lunches (manning.com) – A follow up on the first recommend book which dives deeper into PowerShell scripting specifically
Home – PSConfEU – A fantastic week long conference in Europe, where you can listen and learn about all things PowerShell from the experts.
https://reddit.com/r/PowerShell/. Here you will find questions, and answers, from other members of the PowerShell community, and you may even find that someone has already created what you are trying to achieve!
https://discord.gg/powershell. There are channels here dedicated to all things PowerShell.
https://discord.gg/winadmins. While not specifically dedicated to PowerShell, it has a vast array of members who are very PowerShell savvy.