Difference between $MyInvocation and $PSScriptRoot [duplicate] - powershell

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?

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

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.

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
}

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.

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
}

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.

Very similar to already posted answers, but piping seems more PowerShell-like:
$PSCommandPath | Split-Path -Parent

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.

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
}

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.

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

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"

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.

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.

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

Related

Get a powershell script's location, optionally through symlink

Plenty of people have asked, and gotten several variations of answers to, the question "How do I get the path of the script itself in Powershell?". However, in my situation I have a few utility functions stored in a common module alongside the script but I don't actually run the script from that particular directory, instead I have symlink'd the script to $HOME\bin, which I have in PATH. And I do not want to symlink all the utility libraries into the $HOME\bin directory.
How can I get the path of the "real" script path in Powershell, given that the script the user actually runs (i.e. is found in PATH) can be a symlink?
It's a little clunky, but use the common $PSCommandPath to get the script pathname, then try to look up what it links to. If there's no result then $PSCommandPath is the answer. Otherwise check if it's an absolute link target path; if it is then that is the answer. Otherwise join the path of the symlink with its target. Finally Resolve-Path is used to "remove" the relative part of the merged pathname.
Function Get-RealScriptPath() {
# Get script path and name
$ScriptPath = $PSCommandPath
# Attempt to extract link target from script pathname
$link_target = Get-Item $ScriptPath | Select-Object -ExpandProperty Target
# If it's not a link ..
If(-Not($link_target)) {
# .. then the script path is the answer.
return $ScriptPath
}
# If the link target is absolute ..
$is_absolute = [System.IO.Path]::IsPathRooted($link_target)
if($is_absolute) {
# .. then it is the answer.
return $link_target
}
# At this point:
# - we know that script was launched from a link
# - the link target is probably relative (depending on how accurate
# IsPathRooted() is).
# Try to make an absolute path by merging the script directory and the link
# target and then normalize it through Resolve-Path.
$joined = Join-Path $PSScriptRoot $link_target
$resolved = Resolve-Path -Path $joined
return $resolved
}
Function Get-ScriptDirectory() {
$ScriptPath = Get-RealScriptPath
$ScriptDir = Split-Path -Parent $ScriptPath
return $ScriptDir
}
$ScriptDir = Get-ScriptDirectory

Using PSScriptRoot with Import-CSV

As detailed in:
PowerShell-Import-Local-CSV
$empCsv = Import-Csv "$PSScriptRoot\addEmp.csv"
Is classed as an answer. This doesn't work for me when i run this code inside a function. How can i test this?
You can test your Powershell version with $PSVersionTable.PSVersion.
If it is below version 3.0 you can use:
Split-Path $script:MyInvocation.MyCommand.Path -Parent
to find the directory the current script is running from instead of $PSScriptRoot.
$PSScriptRoot will provide the path for the currently running script/function, so if a script runs a function from another location, $PSScriptRoot will contain that other location.
If you want to get the path of the script that's calling the function, leverage $MyInvocation:
$empCsv = Import-Csv "$(Split-Path $MyInvocation.ScriptName)\addEmp.csv"
Demo
Create the Test.psm1 file.
Create the Caller.ps1 file in a subfolder.
Run Caller.ps1.
$PSScriptRoot will refer to Test.psm1 location, $MyInvocation will refer to Caller.ps1 location.
Test.psm1:
Function Test{
Write-host "PSScriptRoot: $PSScriptRoot"
Write-host "MyInvocation: $(Split-Path $MyInvocation.ScriptName)"
}
Caller.ps1:
Import-Module ..\Test.psm1 -Force
Test

Change directory to the location of the operating script

So, I can find out the directory of the script with
function Get-ScriptDirectory { Split-Path -parent $PSCommandPath }
But I can't manage to change the directory to the folder, where the script is saved. For example
cd d:\MyScripts\
So something like
cd function Get-ScriptDirectory { Split-Path -parent $PSCommandPath }
doesn't work.
And sorry if that is a common and dumb question, but I'm new in powershell and I searched and tried a lot before posting here
You can do it like below. You can include the code sample in your script. Then if you are running the script from other location (by means of fully qualifying path name to the script); $MyInvocation will pickup the invocation location.
From Documentation
$MyInvocation
Contains an information about the current command, such as the name,
parameters, parameter values, and information about how the command was
started, called, or "invoked," such as the name of the script that called
the current command.
Code Sample
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
cd $dir

How to get the current directory of the cmdlet being executed

This should be a simple task, but I have seen several attempts on how to get the path to the directory where the executed cmdlet is located with mixed success. For instance, when I execute C:\temp\myscripts\mycmdlet.ps1 which has a settings file at C:\temp\myscripts\settings.xml I would like to be able to store C:\temp\myscripts in a variable within mycmdlet.ps1.
This is one solution which works (although a bit cumbersome):
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$settingspath = $directorypath + '\settings.xml'
Another one suggested this solution which only works on our test environment:
$settingspath = '.\settings.xml'
I like the latter approach a lot and prefer it to having to parse the filepath as a parameter each time, but I can't get it to work on my development environment. What should I do? Does it have something to do with how PowerShell is configured?
Yes, that should work. But if you need to see the absolute path, this is all you need:
(Get-Item .).FullName
The reliable way to do this is just like you showed $MyInvocation.MyCommand.Path.
Using relative paths will be based on $pwd, in PowerShell, the current directory for an application, or the current working directory for a .NET API.
PowerShell v3+:
Use the automatic variable $PSScriptRoot.
The easiest method seems to be to use the following predefined variable:
$PSScriptRoot
about_Automatic_Variables and about_Scripts both state:
In PowerShell 2.0, this variable is valid only in script modules (.psm1). Beginning in PowerShell 3.0, it is valid in all scripts.
I use it like this:
$MyFileName = "data.txt"
$filebase = Join-Path $PSScriptRoot $MyFileName
You can also use:
(Resolve-Path .\).Path
The part in brackets returns a PathInfo object.
(Available since PowerShell 2.0.)
Try :
(Get-Location).path
or:
($pwd).path
Path is often null. This function is safer.
function Get-ScriptDirectory
{
$Invocation = (Get-Variable MyInvocation -Scope 1).Value;
if($Invocation.PSScriptRoot)
{
$Invocation.PSScriptRoot;
}
Elseif($Invocation.MyCommand.Path)
{
Split-Path $Invocation.MyCommand.Path
}
else
{
$Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
}
}
Get-Location will return the current location:
$Currentlocation = Get-Location
I like the one-line solution :)
$scriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
Try this:
$WorkingDir = Convert-Path .
In Powershell 3 and above you can simply use
$PSScriptRoot
If you just need the name of the current directory, you could do something like this:
((Get-Location) | Get-Item).Name
Assuming you are working from C:\Temp\Location\MyWorkingDirectory>
Output
MyWorkingDirectory
Most answers don't work when debugging in the following IDEs:
PS-ISE (PowerShell ISE)
VS Code (Visual Studio Code)
Because in those the $PSScriptRoot is empty and Resolve-Path .\ (and similars) will result in incorrect paths.
Freakydinde's answer is the only one that resolves those situations, so I up-voted that, but I don't think the Set-Location in that answer is really what is desired. So I fixed that and made the code a little clearer:
$directorypath = if ($PSScriptRoot) { $PSScriptRoot } `
elseif ($psise) { split-path $psise.CurrentFile.FullPath } `
elseif ($psEditor) { split-path $psEditor.GetEditorContext().CurrentFile.Path }
For what it's worth, to be a single-line solution, the below is a working solution for me.
$currFolderName = (Get-Location).Path.Substring((Get-Location).Path.LastIndexOf("\")+1)
The 1 at the end is to ignore the /.
Thanks to the posts above using the Get-Location cmdlet.
this function will set the prompt location to script path, dealing with the differents way to get scriptpath between vscode, psise and pwd :
function Set-CurrentLocation
{
$currentPath = $PSScriptRoot # AzureDevOps, Powershell
if (!$currentPath) { $currentPath = Split-Path $pseditor.GetEditorContext().CurrentFile.Path -ErrorAction SilentlyContinue } # VSCode
if (!$currentPath) { $currentPath = Split-Path $psISE.CurrentFile.FullPath -ErrorAction SilentlyContinue } # PsISE
if ($currentPath) { Set-Location $currentPath }
}
You would think that using '.\' as the path means that it's the invocation path. But not all the time. Example, if you use it inside a job ScriptBlock. In which case, it might point to %profile%\Documents.
This is what I came up with. It's an array including multiple methods of finding a path, uses the current location, filters out null\empty results, and returns the first not-null value.
#((
($MyInvocation.MyCommand.Module.ModuleBase),
($PSScriptRoot),
(Split-Path -Parent -Path $MyInvocation.MyCommand.Definition -ErrorAction SilentlyContinue),
(Get-Location | Select-Object -ExpandProperty Path)
) | Where-Object { $_ })[0]
To only get the current folder name, you can also use:
(Split-Path -Path (Get-Location) -Leaf)
To expand on #Cradle 's answer: you could also write a multi-purpose function that will get you the same result per the OP's question:
Function Get-AbsolutePath {
[CmdletBinding()]
Param(
[parameter(
Mandatory=$false,
ValueFromPipeline=$true
)]
[String]$relativePath=".\"
)
if (Test-Path -Path $relativePath) {
return (Get-Item -Path $relativePath).FullName -replace "\\$", ""
} else {
Write-Error -Message "'$relativePath' is not a valid path" -ErrorId 1 -ErrorAction Stop
}
}
I had similar problems and it made me a lot of trouble since I am making programs written in PowerShell (full end user GUI applications) and I have a lot of files and resources I need to load from disk.
From my experience, using . to represent current directory is unreliable. It should represent current working directory, but it often does not.
It appears that PowerShell saves location from which PowerShell has been invoked inside ..
To be more precise, when PowerShell is first started, it starts, by default, inside your home user directory. That is usually directory of your user account, something like C:\USERS\YOUR USER NAME.
After that, PowerShell changes directory to either directory from which you invoked it, or to directory where script you are executing is located before either presenting you with PowerShell prompt or running the script. But that happens after PowerShell app itself originally starts inside your home user directory.
And . represents that initial directory inside which PowerShell started. So . only represents current directory in case if you invoked PowerShell from the wanted directory. If you later change directory in PowerShell code, change appears not to be reflected inside . in every case.
In some cases . represents current working directory, and in others directory from which PowerShell (itself, not the script) has been invoked, what can lead to inconsistent results.
For this reason I use invoker script. PowerShell script with single command inside:
POWERSHELL.
That will ensure that PowerShell is invoked from the wanted directory and thus make . represent current directory. But it only works if you do not change directory later in PowerShell code.
In case of a script, I use invoker script which is similar to last one I mentioned, except it contains a file option:
POWERSHELL -FILE DRIVE:\PATH\SCRIPT NAME.PS1.
That ensures that PowerShell is started inside current working directory.
Simply clicking on script invokes PowerShell from your home user directory no matter where script is located.
It results with current working directory being directory where script is located, but PowerShell invocation directory being C:\USERS\YOUR USER NAME, and with . returning one of these two directories depending on the situation, what is ridiculous.
But to avoid all this fuss and using invoker script, you can simply use either $PWD or $PSSCRIPTROOT instead of . to represent current directory depending on weather you wish to represent current working directory or directory from which script has been invoked.
And if you, for some reason, want to retrieve other of two directories which . returns, you can use $HOME.
I personally just have invoker script inside root directory of my apps I develop with PowerShell which invokes my main app script, and simply remember to never ever change current working directory inside my source code of my app, so I never have to worry about this, and I can use . to represent current directory and to support relative file addressing in my applications without any problems.
This should work in newer versions of PowerShell (newer than version 2).
Mine was a short, so unplug everything but USB from it and recompile

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?
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
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.
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
}
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.
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
}
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.
Very similar to already posted answers, but piping seems more PowerShell-like:
$PSCommandPath | Split-Path -Parent
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.
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
}
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.
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
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"
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.
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.
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