PowerShell – Running Your App’s Scripts Out Of A HOME Folder (+) Dot-Source All .ps1 Files In Folder Recursively

First, I cannot completely take credit for this post. This is a technique that Andrew (my Team Lead as of this writing) used in his code, which I liked very much and hence am sharing with you with my own thoughts added.

The technique that you are about to see works even with UNC paths.

KillerApp – Running scripts from its home folder

Let us say you have an App named KillerApp. You have all the scripts (*.ps1 files) in the folder c:\Temp\KillerApp for example. You have to dot-source all the scripts within the folder so that the app can use the KillerApp’s functions within the .ps1 files seamlessly. The right approach is to create a module and import the module but for various reasons, that method is not yet the norm in most shops for various reasons and is especially true with budding PowerShell adapters.

This post assumes that your requirement is to stay with .ps1 files with individual functions/scripts and not create modules for now.

Worst-case scenario (lots of hard-coded references):

In the worst case scenario, you have scripts with hard-coded path references all over the place and make calls like this within your code:

c:\Temp\KillerApp\Get-KillerAppReturn.ps1 -Param1 "MyVal1"
c:\Temp\KillerApp\Save-KillerAppObject -Param1 "MyVal1" -Param2 "MyVal2"
#Do something
#.....
c:\Temp\KillerApp\Set-KillerAppObject.ps1 -Param1 "MyVal1"
#Do something else
#.....
c:\Temp\KillerApp\Remove-KillerAppObject.ps1 -Param1 "MyVal1"
#Do yet another thing
#.....and make more calls
#...

Dot-sourcing (some hard-coded references):

Assuming KillerApp’s .ps1 files are all functions and the home is now C:\Temp\KillerApp, we could dot-source all the files in the folder somewhat like this within our main script and at this point, the hard-coding only is done when dot-sourcing the functions

. c:\Temp\KillerApp\Get-KillerAppReturn.ps1
. c:\Temp\KillerApp\Save-KillerAppObject.ps1
. c:\Temp\KillerApp\Set-KillerAppObject.ps1
. c:\Temp\KillerApp\Remove-KillerAppObject.ps1

Then, in your code, you would proceed to use the killer functions without any path references. The worst-case scenario from above not looks like like below which is slightly better.

Get-KillerAppReturn.ps1 -Param1 "MyVal1"
Save-KillerAppObject -Param1 "MyVal1" -Param2 "MyVal2"
#Do something
#.....
Set-KillerAppObject.ps1 -Param1 "MyVal1"
#Do something else
#.....
Remove-KillerAppObject.ps1 -Param1 "MyVal1"
#Do yet another thing
#.....and make more calls
#...

What if the KillerApp’s home folder suddenly moves?

Now, how do you make your app work with all its scripts without having to change code if you move it to a different folder?

You could now change the initial script that that dot-sources all the functions to alter the path and you are all set. This is still not ideal because you have to make a change when the location changed.

Create a PSDrive for KillerApp

You could create a PSDrive for KillerApp in the entry-point script as shown below. The basic syntax is like this:

New-PSDrive -Name KillerApp `
    -PSProvider 'FileSystem' `
    -Root C:\Temp\KillerApp `
    -Description "Killer App Home"

Now you could do this like this:

PS C:\Users\MyId> cd KillerApp:

PS KillerApp:> dir

    Directory: C:\Temp\KillerApp

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        2/11/2017   3:43 PM           1065 Invoke-KillerApp.ps1
-a----        2/11/2017   3:11 PM            147 Get-KillerAppObject.ps1
-a----        2/11/2017   3:32 PM            337 Set-KillerAppObject.ps1
-a----        2/11/2017   3:11 PM            327 Save-KillerAppObject.ps1
-a----        2/11/2017   3:33 PM            567 Remove-KillerAppObject.ps1

Let us go ahead and remove the drive that we just created and try to use it for real for our app (we need to change to another drive like C: before we can remove the drive we are on!)

PS KillerApp:\> C:
PS C:\Users\Jana> Remove-PSDrive KillerApp

Putting it all together: NO hard-coded paths at all!

Using the concept above, assuming Invoke-KillerApp is our entry point script, it would look something like what is below. The current path of the executing scripts folder is captured and a drive is created for KillerApp. All the necessary scripts within the newly created drive are dot-sourced and used. Later, the mapped drive is removed.


Function Invoke-KillerApp
{
    #Get the path where this script (Invoke-KillerApp) is
    $killerAppHome = Split-Path -parent $PSCommandPath

    #Store current location (to restore later)
    $loc = Get-Location

    #Make a new drive with KillerApp's folder as the root!
    New-PSDrive `
            -Name KillerApp `
            -PSProvider FileSystem `
            -Root $killerAppHome `
            -Description "Killer App Home" | Out-Null

    #CD to the KillerApp folder
    CD KillerApp:\

    #Dot-source all KillerApp functions
    . .\Get-KillerAppObject.ps1
    . .\Save-KillerAppObject.ps1
    . .\Set-KillerAppObject.ps1
    . .\Remove-KillerAppObject.ps1

    #Call KillerApp functions!
    Set-KillerAppObject -InputObject "Calling Set-KillerAppObject...."

    #Restore saved location
    CD $loc

    #Be a good citizen and remove the drive we created
    Remove-PSDrive -Name KillerApp

}

You can now move the app’s folder and its scripts anywhere and the application would continue to work with no changes. Now, that is how I like to build applications!

Make it even more generic

As good as the above might seem, it is still not good enough when you have to list every script you are referencing to dot-source it first. You would have to alter the “Invoke” script to dot-source every new script that you add or rename.

You could use a technique like the one shown in this article to dot-source all related functionality in a folder/sub-folder and still have no dependencies on the location of where your application is.

Here is a starting point for you. The function below dot-source everything with a .ps1 file extension within the passed-in folder and the sub-folders recursively.

function Initialize-KillerApp
{
    [CmdletBinding()]
    param(
        [ValidateScript({Test-Path $_ -PathType Container})]
        [Parameter(Mandatory=$true)]
        [string] $KillerAppRootPath
    )

    [string] $fn = $MyInvocation.MyCommand
    [string] $stepName = "Begin [$fn]"

    try
    {

        $stepName = "[$fn]: Validate parameters"
        #--------------------------------------------
        Write-Verbose $stepName  

        $stepName = "[$fn]: Read XML file [$KillerAppRootPath]"
        #--------------------------------------------
        Write-Verbose $stepName  

        #Dot-source everything except KillerApp.ps1
        #  which will be a script (not function), for example
        Get-ChildItem -LiteralPath $KillerAppRootPath `
             -Recurse -Filter '*.ps1' `
            | Where-Object { $_.FullName -ne 'KillerApp.ps1' } `
            | foreach {"Loading $($_.FullName)"; . $_.FullName}

    }
    catch
    {
        [Exception]$ex = $_.Exception
        Throw "Unable to load KillerApp code. Error in step: `"{0}]`" `n{1}" -f `
                        $stepName, $ex.Message
    }
    finally
    {
        #Return value if any

    }
}

With the above code, you don’t even need the PSDrive mapping anymore except for convenience to reference other types of files (like config files) in the root folder with shorter references because all the functions in the folder will be loaded by the above function.

..but wait, there is even more!

As it turns out, if you have a ton of files you are dot-sourcing, especially out of a network, the above method can be slow. Personally, I found a dramatic improvement from using the procedure outlined here to dot-source.

https://becomelotr.wordpress.com/2017/02/13/expensive-dot-sourcing/

Additional Note:

If you use the latest generic method noted, the dot-sourced functions will be scoped at the local level and will not persist for continuous use. You would have use the “Global:”  prefix as show in this example:

function Global:Test-AppParameter
{
[CmdletBinding()]
param(
...

Conclusion:

I have over-simplified too many things above and the examples are illustrative but do not follow standards/conventions. I would encourage you to use modules and also embrace coding standards. Also, your scripts should not cause any side-effect if someone accidentally dot-sources it. Ideally, they should be functions that take parameters that are validated.

One thought on “PowerShell – Running Your App’s Scripts Out Of A HOME Folder (+) Dot-Source All .ps1 Files In Folder Recursively

Leave a comment