Consider the following sequence of commands upon opening a PowerShell terminal:
PS C:\Users\username> cd source
PS C:\Users\username\source> $dir = ".\temp"
PS C:\Users\username\source> [System.IO.Path]::GetFullPath($dir)
C:\Users\username\temp
Now this:
PS C:\Users\username> cd source
PS C:\Users\username\source> powershell
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
Try the new cross-platform PowerShell https://aka.ms/pscore6
PS C:\Users\username\source> $dir = ".\temp"
PS C:\Users\username\source> [System.IO.Path]::GetFullPath($dir)
C:\Users\username\source\temp
Why does PowerShell interpret "." relative to the directory in which PowerShell was started, rather than the current directory? How can I get it to interpret "." relative to the current directory?
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, file-system-native[2] 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.
Caveat:
For resolving relative paths, the solutions below assume that PowerShell's current location (as output by Get-Location
/ reflected in $PWD
) is a file-system location, i.e. a directory - as is typical.
If this assumption cannot be made (in the unlikely event that the current location is a registry location, for instance), an explicit reference to the FileSystem
provider's location is required, using (Get-Location -PSProvider FileSystem).ProviderPath
in lieu of $PWD.ProviderPath
below. Notably, this precludes the Convert-Path
and New-Item
approaches below.
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()
and [IO.File]::WriteAllText()
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.
Use Convert-Path
:
[IO.File]::ReadAllText((Convert-Path -LiteralPath someFile.txt))
That Convert-Path
and Resolve-Path
only work with existing paths (as of PowerShell (Core) 7.2) is unfortunate; providing an opt- in for nonexistent path has been proposed in GitHub issue #2993.
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 GitHub issue #10494.
The simplest and robust approach is to use New-Item
to let PowerShell create the item beforehand, which returns a file-system information object whose .FullName
property reflects the full, native path:
# For a *directory* path, use -Type Directory
[IO.File]::WriteAllText(
(New-Item -Type File ./someFile.txt).FullName,
"sample content"
)
If you don't want to create the file/directory in PowerShell beforehand, there are several approaches:
$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]::WriteAllText("$PWD/someFile.txt", "sample content")
$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]::WriteAllText("$($PWD.ProviderPath)/someFile.txt", "sample content")
(Get-Location -PSProvider FileSystem).ProviderPath
[IO.File]::WriteAllText(
"$((Get-Location -PSProvider FileSystem).ProviderPath)/someFile.txt"),
"sample content"
)
That is, you may have to resolve to a full, file-system native a path that is given to you, which may or may not be relative, and may or may not be based on a PowerShell-specific drive (which .NET knows nothing about).
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):
Note:
$PWD.ProviderPath
is used below to refer to the current file-system location's native path. As noted, this presumes that PowerShell's current location is indeed a file-system location, as is typical; for full robustness, use (Get-Location -PSProvider FileSystem).ProviderPath
instead.In PowerShell (Core) v6+, which builds on .NET Core / .NET 5+, 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)
In Windows PowerShell, you additionally need [IO.Path]::Combine()
:
Note: In the simplest case, i.e. if your relative paths never start with \
(or /
)[3] and you don't care about normalizing the resulting path (by resolving .\
or ..\
components and/or having /
normalized to \
), [IO.Path]::Combine()
alone is enough:
# !! Note the limitations.
$dir = "temp"
[IO.Path]::Combine($PWD.ProviderPath, $dir)
Combining [IO.Path]::Combine()
with [IO.Path]::GetFullPath()
overcomes these limitations:
# Robust solution
$dir = "./temp"
[IO.Path]::GetFullPath([IO.Path]::Combine($PWD.ProviderPath, $dir))
[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.
[2] That is, paths based on PowerShell-specific drives (see New-PSDrive
) must be translated to paths based on drives known to all processes.
[3] As Theo notes, [IO.Path]::Combine()
considers a (non-UNC) path that starts with \
(or /
) a full one, despite being rooted only relative to the current drive. Therefore, such paths must be prefixed with the drive spec. of the native file-system directory underlying PowerShell's current location to form a truly full path.