ChatGPT解决这个技术问题 Extra ChatGPT

What's the best way to determine the location of the current PowerShell script?

Whenever I need to reference a common module or script, I like to use paths relative to the current script file. That way, my script can always find other scripts in the library.

So, what is the best, standard way of determining the directory of the current script? Currently, I'm doing:

$MyDir = [System.IO.Path]::GetDirectoryName($myInvocation.MyCommand.Definition)

I know in modules (.psm1) you can use $PSScriptRoot to get this information, but that doesn't get set in regular scripts (i.e. .ps1 files).

What's the canonical way to get the current PowerShell script file's location?

Your solution $PSScriptRoot IS the right one, no need to read further.

P
Peter Mortensen

PowerShell 3+

# This is an automatic variable set to the current file's/module's directory
$PSScriptRoot

PowerShell 2

Prior to PowerShell 3, there was not a better way than querying the MyInvocation.MyCommand.Definition property for general scripts. I had the following line at the top of essentially every PowerShell script I had:

$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition

What does Split-Path used for here?
Split-Path is used with the -Parent parameter to return the current directory without the currently executing script's name.
Note: with PowerShell on Linux/macOS, your script must have a .ps1 extension for PSScriptRoot/MyInvocation etc to be populated. See bug report here: github.com/PowerShell/PowerShell/issues/4217
Quibble with a potentially interesting aside: The closer v2- approximation of $PSScriptRoot is (Split-Path -Parent applied to) $MyInvocation.MyCommand.Path, not $MyInvocation.MyCommand.Definition, though in the top-level scope of a script they behave the same (which is the only sensible place to call from for this purpose). When called inside a function or script block, the former returns the empty string, whereas the latter returns the function body's / script block's definition as a string (a piece of PowerShell source code).
Split-Path documentation says "The Parent parameter is the default split location parameter." and IME -Parent is indeed not necessary.
Z
Zombo

If you are creating a V2 Module, you can use an automatic variable called $PSScriptRoot.

From PS > Help automatic_variable

$PSScriptRoot
       Contains the directory from which the script module is being executed.
       This variable allows scripts to use the module path to access other
       resources.

This is what you need in PS 3.0: $PSCommandPath Contains the full path and file name of the script that is being run. This variable is valid in all scripts.
Just tested $PSScriptRoot and working as expected. However, it would give you an empty string if you run it at command line. It would only give you result if used in a script and script is executed. That's what it is meant for .....
I'm confused. This answer says to use PSScriptRoot for V2. Another answer says PSScriptRoot is for V3+, and to use something different for v2.
@user $PSScriptRoot in v2 is only for modules, if you're writing 'normal' scripts not in a module you need $MyInvocation.MyCommand.Definition, see top answer.
@Lofful I said in "v2" it was only defined for modules. You're saying it's defined outside modules in v3. I think we're saying the same thing. : )
Z
Zombo

For PowerShell 3.0

$PSCommandPath
    Contains the full path and file name of the script that is being run. 
    This variable is valid in all scripts.

The function is then:

function Get-ScriptDirectory {
    Split-Path -Parent $PSCommandPath
}

Even better, use $PSScriptRoot. It is the current file's/module's directory.
This command includes the filename of the script, which threw me off until I realized that. When you are wanting the path, you probably don't want the script name in there too. At least, I can't think of a reason you would want that. $PSScriptRoot does not include the filename (gleaned from other answers).
$PSScriptRoot is empty from a regular PS1 script. $PSCommandPath works, though. Behavior of both is expected per the descriptions given in other posts. Can also just use [IO.Path]::GetDirectoryName($PSCommandPath) to get the script directory without the file name.
P
Peter Mortensen

For PowerShell 3+

function Get-ScriptDirectory {
    if ($psise) {
        Split-Path $psise.CurrentFile.FullPath
    }
    else {
        $global:PSScriptRoot
    }
}

I've placed this function in my profile. It works in ISE using F8/Run Selection too.


It should be $script:PSScriptRoot instead of $global:PSScriptRoot which only works in ISE but not in a plain PowerShell.
P
Peter Mortensen

Maybe I'm missing something here... but if you want the present working directory you can just use this: (Get-Location).Path for a string, or Get-Location for an object.

Unless you're referring to something like this, which I understand after reading the question again.

function Get-Script-Directory
{
    $scriptInvocation = (Get-Variable MyInvocation -Scope 1).Value
    return Split-Path $scriptInvocation.MyCommand.Path
}

This gets the current location where the user is running the script. Not the location of the script file itself.
function Get-Script-Directory { $scriptInvocation = (Get-Variable MyInvocation -Scope 1).Value return Split-Path $scriptInvocation.MyCommand.Path } $hello = "hello" Write-Host (Get-Script-Directory) Write-Host $hello Save that and run it from a different directory. You'll show the path to the script.
That's a good function, and does what I need, but how do I share it and use it in all my scripts? It's a chicken and egg problem: I'd like to use a function to find out my current location, but I need my location to load the function.
NOTE: The invokation of this function must be at the top level of your script, if it is nested within another function, then you have to change the "-Scope" parameter to designate how deep in the call stack you are.
J
Julian

I use the automatic variable $ExecutionContext. It will work from PowerShell 2 and later.

 $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath('.\')

$ExecutionContext Contains an EngineIntrinsics object that represents the execution context of the Windows PowerShell host. You can use this variable to find the execution objects that are available to cmdlets.


This is the only one it worked for me, trying to feed powershell from STDIN.
This solution also works properly when you are in a UNC path context.
This is apparently picking up the working directory - rather than where the script is located ?
@monojohnny Yes this is basically the current working directory and will not work when calling a script from another location.
P
Peter Mortensen

Very similar to already posted answers, but piping seems more PowerShell-like:

$PSCommandPath | Split-Path -Parent

Why do this when there is already an automatic variable for this $PSScriptRoot? $PSCommandPath & $PSScriptRoot were both introduced in PowerShell v3 so technically none of this helps the OP since they noted v2, but IS helpful for future people who stumble across this question like I did
P
Peter Mortensen

It took me a while to develop something that took the accepted answer and turned it into a robust function.

I am not sure about others, but I work in an environment with machines on both PowerShell version 2 and 3, so I needed to handle both. The following function offers a graceful fallback:

Function Get-PSScriptRoot
{
    $ScriptRoot = ""

    Try
    {
        $ScriptRoot = Get-Variable -Name PSScriptRoot -ValueOnly -ErrorAction Stop
    }
    Catch
    {
        $ScriptRoot = Split-Path $script:MyInvocation.MyCommand.Path
    }

    Write-Output $ScriptRoot
}

It also means that the function refers to the Script scope rather than the parent's scope as outlined by Michael Sorens in one of his blog posts.


Thanks! The "$script:" was what I needed to get this to work in Windows PowerShell ISE.
Thanks for this. This was the only one that works for me. I usually have to CD into the directory the script is in before things like Get-location work for me. I'd be interested to know why PowerShell doesn't automatically update the directory.
B
Bruce Gavin

I needed to know the script name and where it is executing from.

Prefixing "$global:" to the MyInvocation structure returns the full path and script name when called from both the main script, and the main line of an imported .PSM1 library file. It also works from within a function in an imported library.

After much fiddling around, I settled on using $global:MyInvocation.InvocationName. It works reliably with CMD launch, Run With Powershell, and the ISE. Both local and UNC launches return the correct path.


Split-Path -Path $($global:MyInvocation.MyCommand.Path) worked perfect thanks. The other solutions returned the path of the calling application.
Trivial note: in ISE calling this function using F8/Run Selection will trigger a ParameterArgumentValidationErrorNullNotAllowed exception.
C
Carsten

I always use this little snippet which works for PowerShell and ISE the same way:

# Set active path to script-location:
$path = $MyInvocation.MyCommand.Path
if (!$path) {$path = $psISE.CurrentFile.Fullpath}
if ($path)  {$path = Split-Path $path -Parent}
Set-Location $path

Split-Path part looks good, but is quite slow. If you need it fast then go with RegEx like this: $path = [regex]::Match($path, '.+(?=\)').value
R
Randy

Using pieces from all of these answers and the comments, I put this together for anyone who sees this question in the future. It covers all of the situations listed in the other answers, and I've added another one I found as a fail-safe.

function Get-ScriptPath()
{
    # If using PowerShell ISE
    if ($psISE)
    {
        $ScriptPath = Split-Path -Parent -Path $psISE.CurrentFile.FullPath
    }
    # If using PowerShell 3.0 or greater
    elseif($PSVersionTable.PSVersion.Major -gt 3)
    {
        $ScriptPath = $PSScriptRoot
    }
    # If using PowerShell 2.0 or lower
    else
    {
        $ScriptPath = split-path -parent $MyInvocation.MyCommand.Path
    }

    # If still not found
    # I found this can happen if running an exe created using PS2EXE module
    if(-not $ScriptPath) {
        $ScriptPath = [System.AppDomain]::CurrentDomain.BaseDirectory.TrimEnd('\')
    }

    # Return result
    return $ScriptPath
}

P
Peter Mortensen

I found that the older solutions posted here didn't work for me on PowerShell V5. I came up with this:

try {
    $scriptPath = $PSScriptRoot
    if (!$scriptPath)
    {
        if ($psISE)
        {
            $scriptPath = Split-Path -Parent -Path $psISE.CurrentFile.FullPath
        }
        else {
            Write-Host -ForegroundColor Red "Cannot resolve script file's path"
            exit 1
        }
    }
}
catch {
    Write-Host -ForegroundColor Red "Caught Exception: $($Error[0].Exception.Message)"
    exit 2
}

Write-Host "Path: $scriptPath"

R
Rudi Visser

You might also consider split-path -parent $psISE.CurrentFile.Fullpath if any of the other methods fail. In particular, if you run a file to load a bunch of functions and then execute those functions with-in the ISE shell (or if you run-selected), it seems the Get-Script-Directory function as above doesn't work.


$PSCommandPath will work in the ISE as long as you save the script first and execute the whole file. Otherwise, you're not actually executing a script; you're just "pasting" commands into the shell.
@Zenexer I think that was my goal at the time. Although if my goal didn't match up with the original one, this might not be too helpful except to the occasional Googlers...
z
zumalifeguard

If you want to load modules from a path relative to where the script runs, such as from a "lib" subfolder", you need to use one of the following:

$PSScriptRoot which works when invoked as a script, such as via the PowerShell command $psISE.CurrentFile.FullPath which works when you're running inside ISE

But if you're in neither, and just typing away within a PowerShell shell, you can use:

pwd.Path

You can could assign one of the three to a variable called $base depending on the environment you're running under, like so:

$base=$(if ($psISE) {Split-Path -Path $psISE.CurrentFile.FullPath} else {$(if ($global:PSScriptRoot.Length -gt 0) {$global:PSScriptRoot} else {$global:pwd.Path})})

Then in your scripts, you can use it like so:

Import-Module $base\lib\someConstants.psm1
Import-Module $base\lib\myCoolPsModule1.psm1
#etc.

M
Milen
function func1() 
{
   $inv = (Get-Variable MyInvocation -Scope 1).Value
   #$inv.MyCommand | Format-List *   
   $Path1 = Split-Path $inv.scriptname
   Write-Host $Path1
}

function Main()
{
    func1
}

Main