Get-ScriptDirectory to the Rescue

Get-ScriptDirectory to the Rescue

  • Comments 17

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\test


Mode 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.ps1


PS>
Get-Content Invoke-Test.ps1
. .\LibraryTest.ps1
echo "I Love PowerShell"
PS>
PS>
Get-Content LibraryTest.ps1
function echo ($msg)
{ write-host $msg
}
PS>
PS>
C:\temp\test\Invoke-Test.ps1
I Love PowerShell
PS>
PS>
Set-Location subdir
PS>
C:\temp\test\Invoke-Test.ps1
The term '.\LibraryTest.ps1' is not recognized as a cmdlet, function, opera
ble program, or script file. Verify the term and try again.
At C:\temp\test\Invoke-Test.ps1:1 char:2
+ . <<<< .\LibraryTest.ps1
The 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>
t
72
PS>
function t { (Get-Variable -Scope 0).Count }
PS>
t
17
PS>
# 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>
t

Name Value
---- -----
? True
args {}
ConsoleFileName
Culture en-US
ExecutionContext System.Management.Automation.EngineIntrin...
false False
HOME E:\Users\jsnover.NTDEV
Host System.Management.Automation.Internal.Hos...
input System.Array+SZArrayEnumerator
MaximumVariableCount 4096
MyInvocation System.Management.Automation.InvocationInfo
null
PID 960
PSHOME E:\Windows\system32\WindowsPowerShell\v1.0\
ShellId Microsoft.PowerShell
true True
UICulture 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.ps1


MyCommand : t1.ps1
ScriptLineNumber : 1
OffsetInLine : 9
ScriptName :
Line : .\t1.ps1
PositionMessage :
At line:1 char:9
+ .\t1.ps1 <<<<
InvocationName : .\t1.ps1
PipelineLength : 1
PipelinePosition : 1



PS>
# Note the LACK of a PATH. Let's explore the structure of MyInvocation
PS> # to see if we can find one.
PS> $MyInvocation |Get-Member -type Property


TypeName: System.Management.Automation.InvocationInfo

Name 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.ScriptInfo

Name 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.ps1


Path : C:\Temp\test\subdir\t2.ps1
Definition : C:\Temp\test\subdir\t2.ps1
Name : t2.ps1
CommandType : ExternalScript

PS>
# 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.ps1
function Get-ScriptDirectory
{
$Invocation = (Get-Variable MyInvocation -Scope 1).Value
Split-Path $Invocation.MyCommand.Path
}

$path = Join-Path (Get-ScriptDirectory) LibraryTest.ps1
. $path
echo "I Love PowerShell"
PS>
PS>
C:\Temp\test\Invoke-Test.ps1
I Love PowerShell
PS>
PS>
Set-Location subdir
PS>
PS>
C:\Temp\test\Invoke-Test.ps1
I Love PowerShell
PS>
PS>
# If it can work there, it can work anywhere!

 

I just love this stuff!!!!

 

Jeffrey Snover [MSFT]
Windows Management Partner Architect
Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Leave a Comment
  • Please add 5 and 7 and type the answer here:
  • Post
  • So being a team member doesn't make one know everything.  

    I put that in my startup script a long time ago.  Never thought that it needed to be noted.  Your blog made me realize that many things in PS are not clearly documented (It took me a bit of time to discover MyInvocation.Path.)

    It just proves, once again, that everything is there.  It just tazkes a bit of time and thought to uncover how to do it.

    Hurray for PowerShell!  Hooray for .NET!

    Great stuff Jeffrey / PS Team.

  • Discussed in the newsgroup a few weeks ago: http://groups.google.com/group/microsoft.public.windows.powershell/browse_thread/thread/e57016f37273b1cb/6de1249b8b86936c#6de1249b8b86936c

  • > Discussed in the newsgroup a few weeks ago:

    Doh!!!!

    Well I guess it proves this point:

    ... you can always just post a question to our newsgroup Microsoft.Public.Windows.PowerShell and the community will help.

    Jeffrey Snover [MSFT]

    Windows Management Partner Architect

    Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell

    Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

  • It is very nice to know the trick about using the -scope on get-variable.  Thanks for pointing that out.  What we tend to do in our scripts (and we use a number of dot sourced PowerShell libraries) is this:

    $ScriptDir = split-path -parent $MyInvocation.MyCommand.Path

    . "$ScriptDir\TestUtils.ps1"

    at the top of each script.

  • Another simple solution could be:

    push-location myScriptPath

    ...

    pop-location

  • Thanks!  That tip saved the day for me.  I needed to run a powershell script from a UNC path which was loading an assembly from the same UNC path as the script.  It worked flawlessly!

  • You need to &quot;Dot source&quot; the file. Basically, its &quot;. &lt;filename&gt;&quot;. Check &quot;Get-ScriptDirectory

  • Going to the parent scope for the script-level $MyInvocation variable does not always work. I just had this situation:

    function Test()

    {

       $inv = (Get-Variable MyInvocation -Scope 1).Value

       $inv.MyCommand | Format-List *

    }

    function Test2()

    {

       Test

    }

    Test2

    Where the parent scope is actually the Test2 function not the script scope. You can always get the script scope $MyInvocation variable by using the $script namespace prefix:

    $scriptInvocation = $script:MyInvocation

    This works when called from any function scope no matter how deeply nested.

  • Let me start by saying I really, really  appreciate the information in this post - but I did run into the same problem as Hristo.

    I think it was a disservice to make a function for this trick.  A function suggests it can be referenced from numerous places, but clearly it will only work if called from the main script.  Also, I don't see much point in calling this repeatedly - the answer doesn't change no matter how often you call it.  

    I think it makes much more sense to just put these two lines at the beginning of a script and leave the variable available for the duration of its execution:

    $Invocation = (Get-Variable MyInvocation -Scope 0).Value

    $ScriptPath = Split-Path $Invocation.MyCommand.Path

    Then later on you just make reference to it like so:

    $ReportPath = Join-Path $ScriptPath $ReportFile

    Not only is this much simpler, it will also work reliably from anywhere within the program.

  • How do you get your input text to be yellow and your output to be white????

  • And I thought that %0.. and %~dp0 were CMD.exe voodoo.

    The script directory is so often used, I would argue that the lack of an automatic variable is an over site. Now that Version 2 is out, is there an easier way or is it just as convoluted?

  • Based on this article I now just use this one line to grab the script directory:

    # Get the directory that this script is in.

    $ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path -Parent

    Nice and easy :)

  • Hi there

    I am fairly new to powershell so my actions are quite basic.

    I have made a powershell script as below:

    get-service ‘Netback*’ -computername SERVER1 > server1.txt

    get-service ‘Netback*’ -computername SERVER2 > server2.txt

    Now, thats fine for a couple of machines, but when I have a whole list its obviously harder.

    If I copy the files to a txt file, and then run this command:

    Get-Content server.txt | ForEach-object {get-service ‘Netback*’}, I get the following display:

    Status               Name                   Displayname

    Running         NetBackup…      Netbackup Client Service

    So to my question:

    How am I able to list any one of the machines in my server.txt file so that I know which server it’s referring to?  It’s probably simple, but I can’t work it out.

    server.txt would consist of:

    SERVER1

    SERVER2

    Thanks

    Justin

  • "I just love this stuff!!!!"

    Give us a break.

    Powershell should use C# syntax.

  • Here is a puzzle for you:

    ################################################################

    #$connectionString = "........"

    function Get-ScriptDirectory

    {

       Split-Path $script:MyInvocation.MyCommand.Path

    }

    $dir = Get-ScriptDirectory

    Add-PSSnapin SqlServerCmdletSnapin100

    Add-PSSnapin SqlServerProviderSnapin100

    $connection = New-Object -TypeName System.Data.SqlClient.SqlConnection($connectionString)

    foreach ($f in Get-ChildItem -path $dir | ?{ $_.PSIsContainer } | sort-object )

    {

       $path = $dir + "\" + $f.Name + "\" + "update.sql";

       $path

       Invoke-sqlcmd -InputFile $path

       #$query = Get-Content $path

    }

    $connection.Close()

    ################################################################

    This returns :

    Invoke-sqlcmd : A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance

    name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server)

    1) Remote connectios are active.

    2) Connection string is correct.

Page 1 of 2 (17 items) 12