As js2010's helpful answer states, it is the use of a .NET method that introduces the problem:
.NET's single, process-wide current directory typically and by design[1] differs from PowerShell's runspace-specific one.
This has the following implications:
Since PowerShell itself does reliably interpret .
as the current location (which is PowerShell's generalization of the concept of a current directory that can refer to other types of locations as well, on drives exposed by other PowerShell drive providers, such as the registry provider), you can avoid the problem by using PowerShell commands, if available.
When you do call .NET methods, be sure to resolve any relative paths to absolute ones beforehand or, where supported, additionally supply the current PowerShell filesystem location as a reference directory - this avoids the problem of the mismatched current directories.
- (Another, but suboptimal option is to first set
[Environment]::CurrentDirectory = $PWD.ProviderPath
every time a relative path is passed, but that is clumsy and shouldn't be used if there's a chance that multiple PowerShell runspaces exist in the same process.)
The next section shows how to safely pass relative PowerShell paths to .NET methods, whereas the bottom section solves the specific problem in your question: how to resolve an arbitrary, given PowerShell path to an absolute, native filesystem path.
Passing a known relative PowerShell path safely to a .NET method:
As stated, the discrepancy in current directories requires that an absolute path be passed to .NET methods, arrived at based on PowerShell's current directory.
The examples assume relative path someFile.txt
to be passed to .NET method [IO.File]::ReadAllText()
Note that simple string interpolation is used, with /
(which can be used interchangeably with
) used to join the path components; if the current directory happens to be the root directory, you'll end up with 2 path separators, but that doesn't affect functionality. If you still need to avoid that, however, use the Join-Path
cmdlet instead.
- Simplest, but not fully robust, via
$PWD
(fails if the current directory is based on a PowerShell-specific drive created with New-PsDrive
or if the current location is not a filesystem location):
[IO.File]::ReadAllText("$PWD/someFile.txt")
- More robust: via
$PWD.ProviderPath
(resolves a PowerShell drive-based path to the underlying native filesystem path, but can still fail if the current location is not a filesystem location):
[IO.File]::ReadAllText("$($PWD.ProviderPath)/someFile.txt")
- Fully robust: via
(Get-Location -PSProvider FileSystem).ProviderPath
[IO.File]::ReadAllText("$((Get-Location -PSProvider FileSystem).ProviderPath)/someFile.txt")
Note: The above works with both existent and nonexistent paths; if the path is known to exist - such as with [IO.File]::ReadAllText()
, as opposed to [IO.File]::WriteAllText()
- you can also use the following, but only if you can further assume that the current location is a filesystem location:
[IO.File]::ReadAllText((Convert-Path -LiteralPath someFile.txt))
That Convert-Path
and Resolve-Path
only work with existing paths (as of PowerShell Core 7.0.0-preview.3) is unfortunate; providing an opt-in for nonexistent path has been proposed on GitHub.
Similarly, it would be helpful if Convert-Path
and Resolve-Path
supported a -PSProvider
parameter to allow specifying the target provider explicitly, as Get-Location
already supports - see this suggestion on GitHub.
Resolving a given, arbitrary PowerShell filesystem path to an absolute native path:
If the path exists, use Convert-Path
to resolve any PowerShell filesystem path to an absolute, filesystem-native one:
$dir = "./temp"
Convert-Path -LiteralPath $dir
The related Resolve-Path
cmdlet provides similar functionality, but it doesn't resolve paths based on PowerShell-specific drives (created with New-PsDrive
) to their underlying native filesystem paths.
If the path doesn't exist (yet):
In PowerShell Core, which builds on .NET Core, you can use the new [IO.Path]::GetFullPath()
overload that accepts a reference directory for the specified relative path:
$dir = "./temp"
[IO.Path]::GetFullPath($dir, $PWD.ProviderPath)
Note how the current location's native filesystem path, $PWD.ProviderPath
, is passed as the reference directory.
Caveat: If there's a chance that the current location is on a drive other than a filesystem drive, use
(Get-Location -PSProvider FileSystem).ProviderPath
to reliably refer to the current filesystem location (directory).
In Windows PowerShell, you can use [IO.Path]::Combine()
, but note that you'll have to remove the ./
prefix manually if you don't want it in the resulting path:
$dir = "./temp"
[IO.Path]::Combine($PWD.ProviderPath, $dir -replace '^.[\/]')
[1] While a given process typically has only one PowerShell runspace (session), the potential for multiple ones to coexist in a process means that it is conceptually impossible for all of them to sync their individual working directories with the one and only process-wide .NET working directory. For a more in-depth explanation, see this GitHub issue.