Skip to the main content.

ScriptRunner Blog

Getting started with PowerShell functions

Table of contents

Post Featured Image

PowerShell functions are essential tools that enable you to encapsulate code for reuse, making your scripts more efficient and manageable.

Introduction

PowerShell is a great language. It is really easy for a novice to get started with. The language structure itself, the famous "verb-Noun" structure of the cmdlets (and much more) has been a real game changer. It allowed so many (myself included) to start a programming journey that would never end. 

To learn PowerShell (or really any programming language for that matter), it is a good idea to start with something basic. Something very simple, and build from there. The best thing to do is to find a problem that you have and try to fix it. 

You can start by pasting lines of code that worked for you in a PowerShell console into a file. If you keep using it, you will quickly see things that need to be fixed or improved. The more you use it, the more things you will tweak and improve. (And sometimes break.) 

But, very quickly, your original separate lines of code will become something more robust, less buggy, more user friendly, and most importantly: Easy to reuse!

It is on the topic of reusability that I would like to draw your attention today, as this article is all about reusability.

We will dive into the world of functions by showing how we can build a logging function from a single line of code to a full-fledged function. Besides the syntax, we will go through the thought process of how to create PowerShell functions.

 

Where to start?

Logging can be done in several different places, but today we will only focus on logging to a file.

While this may seem super obvious, let's first define what a log file actually is.

A log file is a text file that records events, processes, or messages
generated by software applications, operating systems, or devices
for tracking and debugging purposes.

To write logs, we need to write text to a file. PowerShell makes this very easy for us by providing the Add-Content cmdlet to do just that. We can use the following line to write the word "woop" to a file named C:\temp\plop.txt


Add-Content -Path "C:\Temp\Plop.txt" -Value "woop"

This assumes that the C:\Temp folder exists. 

We can easily see that this line of code is not suitable for reusability, because it would write the same value woop into the same file C:\Temp\Plop.txt over and over again.

Let's fix this by making the path and message easily changeable.


$Path = "C:\Temp\Plop.txt"
$Message = "woop"
Add-Content -Path $Path -Value $Message 
 

In the line above, we switched from hardcoded values to using variables. But this still doesn't really allow for reusability.
Ideally, we would like to be able to write messages from our scripts to a file. That way, if we schedule this script to run at night, we know in the morning if it ran successfully or not.

This is where functions come in. Since we want to write more than one message in a single script, and to avoid having to copy and paste these three lines over and over again, we will use a function.

In a nutshell, a function is a section that can be uniquely identified by its name. This encapsulates a snippet of code that is executed when called.

This allows you to keep the core content in one central place, and it makes changing things really easy because it is centralized in the function. You can make a change there, and it will be applied wherever the function is called.


function Write-Log ($Path,$Message) {
Add-Content -Path $Message
}

Let's break this down quickly so everyone can follow along. I'm a big believer in the saying "a picture is worth a thousand words," so let's use a picture to illustrate this:

anatomy of a simple function


Function:
is the keyword that notifies PowerShell that the construct below is a 'reusable function'. It is an automatic keyword, and is part of the PowerShell language. 

Write-Log: This is the name of our function. In other words, it is the word that we will use to call our code.

We have the pair of parentheses containing two variables: '$Path' and '$Message'. These are called the parameters.

Notice that they are separated by a comma.

Next, we have a pair of curly brackets '{}' that contain the body of our function, which will be executed each time we call our function using the function name. The beginning of the function is defined by the first opening curly bracket '{' and the end of the function stops at the last closing curly bracket '}'.

Tip: Did you know that everything in between curly brackets '{ }' is called a ScriptBlock ?

Whatever is between the first opening curly bracket, and the last closing curly bracket is the content of our function. This is the core of our function. This is where the the magic happens. (darin liegt das Geheimnis? das ist das ganze Geheimnis)

 In our example, it is actually the following line:


Add-Content -Path $Path -Value $Message

 

Using your first function:
Integrating the logging function in a script

Let's say you have a colleague who has written a script to clean a server of temporary files after a backup. You are not 100% sure that the script actually works. You are not even sure if it will start!

The script is called 'nightScript.ps1' and it looks like this:


#nightScript.ps1
$AllFilesToRemove = Get-ChildItem -Path "C:\Temp\Backup\" -Recurse foreach($file in $AllFilesToRemove) {
Remove-Item -Path $file.FullName -Force
}
Restart-Service -Name "BackupService"

We see here that nothing is actually output to the screen, nor do we have any information written to disk to keep track of what the script actually did or did not do.

This is a perfect opportunity to use your first (basic) logging function. 

In order to use a function, it must be read by the script. Technically, the function can be located anywhere in the script file, but it is a good practice to place it at the very top of the script because it makes it easier to read. 

When you run a PowerShell script, one of the first things the PowerShell engine does is to look for any existing functions in the script and load them into memory. If you modify your function, you must reload your function so that PowerShell has the most recent version in memory.

Info: Loading the function means executing the script again.

Let's prepare our test environment and also improve the script a bit by adding some error handling.

To simulate this, I used a function I wrote for psconfEU 2020 called 'New-FakeFile'. It is quite handy as it allows you to easily create fake files with meaningful names that represent the files we need to backup. You can even specify how many you want and how much space they should take.

I used the following snippet to create 10 files:


Install-module FakeFile
New-FakeFile -NumberOfFiles 10 -TotalSize 10MB -FolderPath C:\Temp\backup 

And let's update the script with our new logging feature and make it a little more robust by adding some basic error handling.


#nightScript_v2.ps1
Function Write-Log ($Message,$Path) {
Add-Content -Path $Path -Value $Message
}
try{
$LogFilePath = $MyInvocation.MyCommand.Source.Replace(".ps1",".log")
Write-log -Message "Starting NightScript" -Path $LogFilePath
$AllFilesToRemove = Get-ChildItem -Path "C:\Temp\Backup\" -Recurse
foreach($file in $AllFilesToRemove)
{
#You can add fake files using:
#install-module FakeFile
#New-FakeFile -NumberOfFiles 10 -TotalSize 10MB -FolderPath C:\Temp\backup
Write-log -Message "Removing file: $($File.FullName)" -Path $LogFilePath
Remove-Item -Path $file.FullName -Force
}
Write-log -Message "Restarting service 'BackUpService'" -Path $LogFilePath
Restart-Service -Name "BackupService" -ErrorAction Stop
}catch{
write-Log -Message "Error Occured: $_" -Path $LogFilePath
}finally{
Write-log -Message "End of script" -Path $LogFilePath
}

Notice that I have moved everything into a try catch finally block. This helps to do two main things:

  • Obviously, it adds error handling. If there is an error somewhere in the catch block, it will automatically stop the execution of the code and fall into the catch block, which will be executed. In this case, we simply write a message mentioning that an error occurred.
  • The second benefit is that it gives our script some structure. The main code goes into the try block, we write any errors that occur into the catch block using the same single line of code, and we write a closing message regardless of whether the script failed or not.

If we run the script as is, we will get the creation of a new log file called 'NightScript_v2.log' just where our script is located, with the execution of our script. It will look something like this:

screenshot of a log file

 Now, if we run the function several times in a row (simulating the script being run over a period of a few days, such as the weekend), we will see that our log file will be filled with more information. We can see that an error occurred at the end of our script, thanks to our 'Error Occurred:' write-log message.

We can also see that it is difficult to see when each task happened. It is also a bit difficult to see if an error occurred in the script or not, as you can see from the log snippet below.

 

One thing we can see is that the script has started and stopped several times, thanks to our "Starting NightScript" and "End of Script" messages.

It is my personal preference to always indicate when the script starts and stops in the log file. One thing that is missing is the time when the script started / stopped. Let's fix that next.

 

A word about conventions

A log file is where we go to find out what happened while a script was running at 3 in the morning. We need to be able to find the information quickly and get accurate information from it. If we have to run the script again to see what actually happened on the system, that means our log feature (or the things we actually log) are not helpful enough to quickly identify what happened and when.

Therefore, it is important that a logging function respects the following 3 things:

  • Consistency
  • Standardization
  • Clear conventions

These 3 rules apply very well to any function you actually write.

We want our function to write a log message, consistently. That is, even if it fails. The message needs to be standardized. One action per line, including errors, and messages should have a timestamp to be able to trace back to when the task actually happened.

And we want clear (and simple!) conventions on which to build. 

Each line should have the same structure, which is divided into 3 main parts.

  • 1. Timestamp
    When did this message/error occur?
    We can get this with the following snippet: get-date -uformat '%Y%m%d-%T'.
  • 2. Severity
    What was the severity of the message, which can be:
    This is a predefined string that can be one of the following values:  Info,Error,Ok
  • 3. Message
    The message we want to send back. 

To make the output easier to read, we fix this by adding a timestamp and the severity to each message that the log function outputs.


function Write-Log ($Message,$Path,$Severity = "INFO") {
$TimeStamp = get-date -uformat '%Y%m%d-%T'
$FullMessage = "$TimeStamp;$Severity;$Message"
Add-Content -Path $Path -Value $FullMessage
}

The code is pretty straightforward, but note the '$Severity = "Info" element that was added as a parameter. The parameter has a default value of "INFO". This means that if the parameter is not used, the value "INFO" will be used. The user is free to overwrite this whenever they want and use whatever value they want.

As in line 28, where we now use the severity switch to indicate that an error has occurred:


write-Log -Message "Error Occured: $_" -Path $LogFilePath -Severity "ERROR"

I saved all the changes in a new file called 'NightScript_v3.ps1'. When we run the script, this is what we see:

screenshot of a script called NightScript_v3.ps1 

We can immediately see when the script started and ended and IF an error occurred.

The other advantage is that since we used the ';' as a separator, it will be very easy to parse this log file in the future!

 

Bonus–Function: provider

Did you know that all the functions currently available to you can be accessed via the function: provider?

Get-ChildItem function: 

This will list all existing functions loaded in your current session.

Here you can see (some of) the functions currently loaded in my session. We can see that some functions of PSHTL together with our write-log function are currently loaded and available in my session.

screenshot Get-ChildItem function

To get only the information of our write-log function, we will use:

Get-ChildItem function:write-log 

screenshot Get-ChildItem function:write-log

It returns the data from the write-log function (if that function has been loaded into your session at least once).

Not very interesting, you might say.

But PowerShell hides a lot of properties from us. You can use the following command to show all of the properties that you want to display:

Get-ChildItem function:write-log | select -Property * 

 Which returns the following on my machine:

There are quite a few properties, but one that should really stand out is the Definition property.

(Get-ChildItem function:write-log).Definition 

It will return the definition (or contents) of your function. This can be especially useful when you are developing your function.

I invite you to try it out. Write your function, load it, list it with:

(Get-ChildItem function:write-log).Definition

Then update the function, do not load the function, and try the same snippet again. You will see that you still have the old version of the definition.

You need to reload (call the function again) to get the latest version in memory and use it.

All of this is to emphasize that this is a common error that can happen: Updating a function and not seeing the changes take effect. This can drive you crazy. I know it did for me when I started writing my first PowerShell scripts 15 years ago! (It still does from time to time, to be completely honest).

So if this happens to you, make sure that the definition is correct, and if not, that you are actually loading the function, and that you are loading it from the correct location.

 

Final note

To summarize, in this article we have learned what a simple function is, how it works, and when we should refactor our code to use it.

We have also seen that we can pass parameters to a function, and even set a default value for a particular parameter if it is not called. We also learned that a function exists only in the session in which it is called, and can be overwritten/updated easily.

In the next article, we will speed things up a bit as we delve into advanced functions and improve our logging even more by adding parameter validation.

 

Good2know

Your ultimate PowerShell Cheat Sheet

Unleash the full potential of PowerShell with our handy poster. Whether you're a beginner or a seasoned pro, this cheat sheet is designed to be your go-to resource for the most important and commonly used cmdlets.

The poster is available for download and in paper form.

PowerShell Poster 2023

Get your poster here!

 

 

Related links 

 

Related posts

13 min read

Getting started with PowerShell functions

PowerShell functions are essential tools that enable you to encapsulate code for reuse, making your scripts more...

7 min read

New ScriptRunner Release Enhances Enterprise IT Automation with Better Security, Transparency and Efficiency

The latest ScriptRunner release enhances Enterprise IT automation with three powerful features: the new Approval Process

13 min read

Mastering Changelog Management with PowerShell

Changelogs keep your software updates clear and organized. Learn the best practices for creating and managing them in...

About the author: