The other day I was writing a script and decided that I wanted to break it into a couple of files and have the main script dot-source a library script in the same directory. Here is the problem that I ran into:
PS> Get-ChildItem Directory: Microsoft.PowerShell.Core\FileSystem::C:\Temp\testMode LastWriteTime Length Name---- ------------- ------ ----d---- 6/19/2007 6:12 AM subdir-a--- 6/19/2007 6:12 AM 47 Invoke-Test.ps1-a--- 6/19/2007 6:12 AM 47 LibraryTest.ps1PS> Get-Content Invoke-Test.ps1. .\LibraryTest.ps1echo "I Love PowerShell"PS>PS> Get-Content LibraryTest.ps1function echo ($msg){ write-host $msg}PS>PS> C:\temp\test\Invoke-Test.ps1I Love PowerShellPS>PS> Set-Location subdirPS> C:\temp\test\Invoke-Test.ps1The term '.\LibraryTest.ps1' is not recognized as a cmdlet, function, operable program, or script file. Verify the term and try again.At C:\temp\test\Invoke-Test.ps1:1 char:2+ . <<<< .\LibraryTest.ps1The term 'echo' is not recognized as a cmdlet, function, operable program,or script file. Verify the term and try again.At C:\temp\test\Invoke-Test.ps1:2 char:5+ echo <<<< "I Love PowerShell"PS>
The problem is that when the script dot sources the library (". .\LibraryTest.ps1") from the current directory, it is the current directory of the process not the directory of the script. That is why it worked when I was in the directory that had the library but it broke when I changed my location to a different directory.
What the script needs to do is to dot-source the library from its own directory (the ScriptDirectory) not the current working directory.
This brings up the question – how do I do that? (Good question!)
I didn't know the answer off the top of my head. Well, as always with PowerShell, there is a way if you think about it for a while. Note that while it is a best practice to go explore and figure this stuff out, you can always just post a question to our newsgroup Microsoft.Public.Windows.PowerShell and the community will help.
So you do you figure this out? Let's first start by seeing what variables are provided to a function. This is a little trickier than it sounds because in PowerShell, if you ask for a variable and it isn't in your function's scope, we look for it in your parent's scope and so on until we reach to top of the stack. So the trick is to only see those variables in your scope. Check this out:
PS> function t { (Get-Variable).Count }PS> t72PS> function t { (Get-Variable -Scope 0).Count }PS> t17PS> # That tells us that the function has access to 72 variables but 17 are in its scope.PS> # PowerShell populates these for each scope automatically.PS> PS> function t { Get-Variable -Scope 0 |sort Name}PS> tName Value---- -----? Trueargs {}ConsoleFileNameCulture en-USExecutionContext System.Management.Automation.EngineIntrin...false FalseHOME E:\Users\jsnover.NTDEVHost System.Management.Automation.Internal.Hos...input System.Array+SZArrayEnumeratorMaximumVariableCount 4096MyInvocation System.Management.Automation.InvocationInfonullPID 960PSHOME E:\Windows\system32\WindowsPowerShell\v1.0\ShellId Microsoft.PowerShelltrue TrueUICulture en-US
The variable $MyInvocation is the one I was looking for so let's explore it and see how it can help me solve this problem. Notice that I'm going to use both test scripts and the interactive shell to explore this. I'm leveraging the fact that all scopes have $MyInvocation so I can use the interactive session to explore its structure but I need a test script to test the actual values for an external script.
PS> Get-Content t1.ps1$MyInvocation | Format-List *PS> .\t1.ps1MyCommand : t1.ps1ScriptLineNumber : 1OffsetInLine : 9ScriptName :Line : .\t1.ps1PositionMessage : At line:1 char:9 + .\t1.ps1 <<<<InvocationName : .\t1.ps1PipelineLength : 1PipelinePosition : 1PS> # Note the LACK of a PATH. Let's explore the structure of MyInvocationPS> # to see if we can find one.PS> $MyInvocation |Get-Member -type Property TypeName: System.Management.Automation.InvocationInfoName MemberType Definition---- ---------- ----------InvocationName Property System.String InvocationName {get;}Line Property System.String Line {get;}MyCommand Property System.Management.Automation.CommandInfo MyC...OffsetInLine Property System.Int32 OffsetInLine {get;}PipelineLength Property System.Int32 PipelineLength {get;}PipelinePosition Property System.Int32 PipelinePosition {get;}PositionMessage Property System.String PositionMessage {get;}ScriptLineNumber Property System.Int32 ScriptLineNumber {get;}ScriptName Property System.String ScriptName {get;}PS> # Notice that MyCommand is a structure (not a simple type) so let's explore it.PS> $MyInvocation.MyCommand |Get-Member -Type Property TypeName: System.Management.Automation.ScriptInfoName MemberType Definition---- ---------- ----------CommandType Property System.Management.Automation.CommandTypes Command...Definition Property System.String Definition {get;}Name Property System.String Name {get;}ScriptBlock Property System.Management.Automation.ScriptBlock ScriptBl...PS> # Looks promising.PS> Get-Content t2.ps1$MyInvocation.MyCommand | Format-List *PS> .\t2.ps1Path : C:\Temp\test\subdir\t2.ps1Definition : C:\Temp\test\subdir\t2.ps1Name : t2.ps1CommandType : ExternalScriptPS> # BINGO!
So with that knowledge I can now write my Get-ScriptDirectory function and use it dot-source a local library properly. Now think about this a second, if you write a function Get-ScriptDirectory and call it, $MyInvocation is going to be changed and reflect the call to that function. So what this function has to do is to work on the $MyInvocation of its parent! Luckly, the PowerShell team thought of that and the Get-Variable cmdlet allows you to specify a SCOPE. If you specify 0, it means the current scope (you saw this earlier). If you specify 1, it means the parent scope (2 means the grandparent and so on). So here it is:
PS> Get-Content Invoke-Test.ps1function Get-ScriptDirectory{ $Invocation = (Get-Variable MyInvocation -Scope 1).Value Split-Path $Invocation.MyCommand.Path}$path = Join-Path (Get-ScriptDirectory) LibraryTest.ps1. $pathecho "I Love PowerShell"PS>PS> C:\Temp\test\Invoke-Test.ps1I Love PowerShellPS>PS> Set-Location subdirPS>PS> C:\Temp\test\Invoke-Test.ps1I Love PowerShellPS>PS> # If it can work there, it can work anywhere!
I just love this stuff!!!!
Jeffrey Snover [MSFT]Windows Management Partner ArchitectVisit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShellVisit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx