That is the relevant information so you’re up to speed on where I am.
Bring on the stupid
The stupid thing that I was doing was that I was manually, visually scanning the script, copying out the function definitions, and pasting them into their own function files.
This was long, this was tedious, and this was not a efficient use of my time.
Especially since the scripts were not laid out as logically as I would have liked.
Personally if I were to have nested functions in a script, I would have them towards the beginning of the file. Together, maybe in a little region that I’ve called “functions”.
Actually, if I have to have a “functions” region, then I have too many functions and I’m going to split them out anyway.
The scripts I was looking at were not laid out this way.
Sure there were what appeared to be a function region but there were also functions further down the script, created just before they were needed.
Hence, manually scanning the whole script, taking a note and a copy of each function before moving on again.
Long, tedious, wasteful.
There is a way!
Like I mentioned at the start, in the “pertinent” region, Chris Dent has a function that we have availed of before that we can use her.
Perfect! Now to automate the final part of manual process. Can we grab the definition of these functions and split them out to a separate file per function?
First question is can we grab the function definitions?
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 the path and name of the function in the function call.
about_scopes
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 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
about_scripts
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
Hello where?
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.
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 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.
about_automatic_variables
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.