Determining $ScriptDir Safely

We had some boiler plate code that we always put into our scripts to set strict mode and to compute $ScriptDir so the script can load other scripts relatively to its location.  This boiler plate code is simple:

#requires –Version 2.0
Set-StrictMode –Version 2.0
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent

However lets say you do this in all your scripts and one script (Parent.ps1) dot-sources another script (PoshLib.ps1) that does the same trick e.g.:

Parent.ps1:
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
"PARENT:  Before dot-sourcing libary ScriptDir is $ScriptDir"
. $ScriptDir\Bin\PoshLib.ps1
"PARENT:  After dot-sourcing libary ScriptDir is $ScriptDir"

PoshLib.ps1
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
"POSHLIB: ScriptDir is $ScriptDir"

Seems innocent enough but check out the output of running Parent.ps1:

PS C:\Users\Keith> C:\Users\Keith\Parent.ps1
PARENT:  Before dot-sourcing libary ScriptDir is C:\Users\Keith
POSHLIB: ScriptDir is C:\Users\Keith\Bin
PARENT:  After dot-sourcing libary ScriptDir is C:\Users\Keith\Bin

What happens here is that because we are “dot sourcing” PoshLib.ps1 into Parent.ps1, its definition of the $ScriptDir variable stomps the one created in Parent.ps1.  Obviously this won’t do.  You could try to create unique variable names for each script but that isn’t ideal and not very maintainable. 

The best answer if you are on PowerShell 2.0 is to just use modules for your libraries.  Modules can have “private” variables that don’t get exported so the variable “stomping” issue never arises.  However, for various reasons, folks can’t always upgrade to PowerShell 2.0.  So here is how you can fix this issue on PowerShell 1.0. 

Essentially what you want to do is to dynamically evaluate the script’s location every time without having to use the longhand:

Split-Path $MyInvocation.MyCommand.Path –Parent

PowerShell allows us to create a shorthand for this using an anonymous scriptblock assigned to a variable like so:

Parent.ps1:
$ScriptDir = { Split-Path $MyInvocation.ScriptName –Parent }
"PARENT:  Before dot-sourcing libary ScriptDir is $(&$ScriptDir)"
. "$(&$ScriptDir)\Bin\PoshLib.ps1"
"PARENT:  After dot-sourcing libary ScriptDir is $(&$ScriptDir)"

PoshLib.ps1
$ScriptDir = { Split-Path $MyInvocation.ScriptName –Parent }
"POSHLIB: ScriptDir is $(&$ScriptDir)"

Note: that once we put the code within the scriptblock we need to switch from $MyInvocation.MyCommand.Path to $MyInvocation.ScriptName.  These changes yield the desired results:

PS C:\Users\Keith> C:\Users\Keith\Parent.ps1
PARENT:  Before dot-sourcing libary ScriptDir is C:\Users\Keith
POSHLIB: ScriptDir is C:\Users\Keith\Bin
PARENT:  After dot-sourcing libary ScriptDir is C:\Users\Keith

This entry was posted in PowerShell. Bookmark the permalink.

6 Responses to Determining $ScriptDir Safely

  1. Andy Arismendi says:

    If you are expecting variable collision or if you’re not sure if it will occur, modules should be used instead of . sourcing in my opinion…

    Parent.ps1:
    $ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
    Write-Host “PARENT: Before importing libary ScriptDir is $ScriptDir”
    Import-Module “$ScriptDir\Bin\PoshLib.psm1”
    Write-Host “PARENT: After importing libary ScriptDir is $ScriptDir”

    PoshLib.psm1
    $ScriptDir = Split-Path $MyInvocation.MyCommand.Path –Parent
    Write-Host “POSHLIB: ScriptDir is $ScriptDir”

    Output:
    PARENT: Before importing libary ScriptDir is C:\Users\andy\Desktop\temp
    POSHLIB: ScriptDir is C:\Users\andy\Desktop\temp\Bin
    PARENT: After importing libary ScriptDir is C:\Users\andy\Desktop\temp

  2. rkeithhill says:

    I agree. In fact, I do recommend using Modules in this post – “The best answer if you are on PowerShell 2.0 is to just use modules …”. However there are cases where you have perhaps a lot of scripts that you can’t justify switching over to modules.

  3. Andy Arismendi says:

    Ah I missed that paragraph, woops. I thought you were suggesting this approach for PS 2.0 because of the boiler plate at the beginning…

    #requires –Version 2.0
    Set-StrictMode –Version 2.0

    Thanks.

  4. rkeithhill says:

    No problem. It’s good to reiterate the benefit of Modules to overcome this sort of issue.

  5. Roman Kuzmin says:

    Alternatively, we can use a function instead of a script block:

    function ScriptRoot { Split-Path $MyInvocation.ScriptName }

    Yes, it does the same but there are a few differences:
    * One may find notation (ScriptRoot) better than &$ScriptRoot (it depends, indeed);
    * Even if strict mode is not enabled, misspelled function names yield errors (misspelled variable names do not).

  6. Pingback: PowerShell $PSScriptRoot vs dot-sourcing | mnaoumov.NET

Leave a comment