TL;DR: Use . $PSScriptRoot\
instead of . .\
if you’re using where the script is as a reference to load other files.
Words: 1033
Time to read: ~ 5 minutes
Update (2019-08-14): Thanks to Cory Knox ( twitter | github | twitch ) pointed out that $PSScriptRoot
is not available in PS2.
I wrote before about our Build Process and how I was in the process of splitting them out. Even how, in the course of splitting out the functions and testing them, I found a bug in our current process.
First split
The first split that I did, I consider relatively simple.
I extracted the functions that were defined in the monolithic script into their own .ps1 file.
Then I created a Pester ( github | twitter) file for each function.
I did this so I could confirm that the functions worked as they were expected to work.
Also so that I could confirm that the functions still worked as they were expected to work if I made any changes.
And I plan to make changes to them in the future.
It was here that I found the bug in the old build process and it was here that I was able to sell the idea of isolating the function definitions and creating tests for them.
However, as with most relatively simple changes, it created an unforeseen problem that I didn’t have a test for.
You have to put back
The functions that I had isolated out from the script and tested were still being called from the script.
So we had to load them back in.
That seems simple enough even if it’s not something that I or others have really looked up before. But I’ve had to so below is my minimal, complete, reproducible example.
Let’s Dot Source them into the script.
Get-Help about_scopes
To add a function to the current scope, type a dot (.) and a space before
about_scopes
the path and name of the function in the function call.
But where
Adding these functions back into the script should be an easy process. The layout of the folders and the scripts for these examples are:
- The script is in the parent folder
Blogs\PSScriptRootVersusDot\script.ps1 - The extracted functions are in the same folder
Blogs\PSScriptRootVersusDot \<extracted functions>.ps1
So our frame of reference is our script, and we know where our functions to import are based on the location of our script.
Luckily PowerShell has us covered there
Get-Help about_scripts
To run a script in the current directory, type the path to the current
about_scripts
directory, or use a dot to represent the current directory, followed by a
path backslash (.).
For example, to run the ServicesLog.ps1 script in the local directory,
type:
.\Get-ServiceLog.ps1
So we need to use a dot (.) to add a function into the current scope and we can use a dot (.) to run a script in the current directory? Let’s check it out…
Careful, this is wrong… 😉
Example 01

function Get-Name {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[String]
$Name
)
begin {}
process {
if (-not ($PSBoundParameters.ContainsKey('Name'))) {
$Name = 'there'
}
[PSCustomObject]@{
Name = $Name
Message = "Hello $Name"
}
}
end {}
}
This function doesn’t really do much but it’s vital for the following function.
function ConvertTo-Message {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[String]
$Receiver
)
begin {
Write-Verbose -Message "[$((Get-Date).TimeOfDay)][$($MyInvocation.MyCommand)] Importing function Get-Name"
. .\Get-Name.ps1
}
process {
$GetNameParams = @{}
if ($PSBoundParameters.ContainsKey('Receiver')) {
$GetNameParams.Add('Name', $Receiver)
Write-Verbose ($GetNameParams | Out-String)
}
$MessageDetails = Get-Name @GetNameParams
"To $($MessageDetails.Name),`n$($MessageDetails.Message)"
}
}
Let’s check this out now…
ConvertTo-Message -Verbose

It works!
So my understanding was, that if you need to import a function, you only need to use dots; Dot source and dot location it.
In this, as with many things, my understanding was wrong.
What I failed to fully grasp was the words “the current directory“. Now most of my scripts so far don’t use the *-Location
cmdlets but one of the build scripts did.
Let’s make a change to our ConvertTo-Message
function to change the location and see how that affects us and whether our importing still works…
Example 02
function ConvertTo-Message02 {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[String]
$Receiver
)
begin {
Push-Location -Path ..\
Write-Verbose "We had to go back up for some reason to $((Get-Location).Path)"
Write-Verbose -Message "[$((Get-Date).TimeOfDay)][$($MyInvocation.MyCommand)] Importing function Get-Name"
. .\Get-Name.ps1
}
process {
$GetNameParams = @{}
if ($PSBoundParameters.ContainsKey('Receiver')) {
$GetNameParams.Add('Name', $Receiver)
Write-Verbose ($GetNameParams | Out-String)
}
$MessageDetails = Get-Name @GetNameParams
"To $($MessageDetails.Name), $($MessageDetails.Message)"
}
end {
Pop-Location
Write-Verbose "We're back to $((Get-Location).Path)!"
}
}
ConvertTo-Message02 -Verbose

Explain or I start swinging
The dot used to represent the location is, as I’ve said before, for the current location. Our ConvertTo-Message02
script changed it’s location as part of the script.
When we used the “dot source dot location” method, we weren’t using where our function is as a frame of reference to import the other functions. We were using what directory we are currently in.
If we change the location or try and call the function from anywhere that is not the directory where the function is defined, the function is not going to work.

Push-Location C:\
ConvertTo-Message -Verbose
Pop-Location

Anywhere
What we can do is actually use our function as a frame of reference.
PowerShell has a lovely automatic variable that we can use for this called $PSScriptRoot
Get-Help about_automatic_variables
$PSItem
about_automatic_variables
Same as $_. Contains the current object in the pipeline object. You can use
this variable in commands that perform an action on every object or on
selected objects in a pipeline.
Example 03

Let’s try again, shall we?
function ConvertTo-Message03 {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[String]
$Receiver
)
begin {
Push-Location -Path ..\
Write-Verbose "We had to go back up for some reason to $((Get-Location).Path)"
Write-Verbose -Message "[$((Get-Date).TimeOfDay)][$($MyInvocation.MyCommand)] Importing function Get-Name"
. $PSScriptRoot\Get-Name.ps1
}
process {
$GetNameParams = @{}
if ($PSBoundParameters.ContainsKey('Receiver')) {
$GetNameParams.Add('Name', $Receiver)
Write-Verbose ($GetNameParams | Out-String)
}
$MessageDetails = Get-Name @GetNameParams
"To $($MessageDetails.Name), $($MessageDetails.Message)"
}
end {
Pop-Location
Write-Verbose "We're back to $((Get-Location).Path)!"
}
}
Let’s try the hard test first. We’ll move to the root of the C:\ drive and try and run it from there.
Push-Location C:\
ConvertTo-Message -Verbose
Pop-Location

Push
Now that I know how to properly use the location of a script as a frame of reference, am I going to use it more?
Yes and no.
Yes, it is great for catching these errors and for short, sharp scripts.
But I should really be pushing these up into a module. We use them often enough that there is no reason why we shouldn’t.
That’s the next action I guess. At least I have more knowledge than when I started.
That’s what counts.