I was wondering if someone could help me understand why does System.IO.FileInfo
behaves differently on Windows than on Linux when handling relative paths.
PS /home/user/Documents> ([System.IO.FileInfo]'./test.txt').FullName
/home/user/Documents/test.txt
PS C:\Users\User\Documents> ([System.IO.FileInfo]'.\test.txt').FullName
C:\Users\User\test.txt
To clarify on the above, there is no difference on how System.IO.FileInfo
handles relative paths on Windows or Linux. The issue is related to [Environment]::CurrentDirectory
not being updated by Push-Location
and Set-Location
, see Mark Bereza's answer for more details on this.
PS /home/user> [Environment]::CurrentDirectory
/home/user
PS /home/user> cd ./Documents/
PS /home/user/Documents> [Environment]::CurrentDirectory
/home/user
And assuming this is a expected behavior, what would be an optimal way to approach our param(...)
blocks on scripts and functions to accept both cases (absolute and relative). I used to type constraint the path parameter to System.IO.FileInfo
but now I can see it is clearly wrong.
This is what I came across, but I'm wondering if there is a better way.
I believe Split-Path -IsAbsolute
will also bring problems if working with Network Paths, please correct me if I'm wrong.
param(
[ValidateScript({
if (Test-Path $_ -PathType Leaf) {
return $true
}
throw 'Invalid File Path'
})]
[string] $Path
)
if (-not (Split-Path $Path -IsAbsolute)) {
[string] $Path = Resolve-Path $Path
}
The underlying problem is that System.IO
.NET methods, including the [System.IO.FileInfo]::new()
constructor you're (implicitly) invoking, use [System.IO.Directory]::GetCurrentDirectory()
to resolve relative paths. For reasons outlined here, this location isn't updated when you change your working directory within PowerShell.
If you want to use .NET methods on a relative path within a PowerShell session, your best bet is to first resolve it to an absolute path using either a PowerShell cmdlet (which will implicitly resolve it relative to the current working directory), or by combining it with the $PWD
automatic variable.
Using cmdlets like Resolve-Path
, Convert-Path
, and Get-Item
is the most straight-forward way to accomplish this, the primary difference between them (for this purpose) being their return type: PathInfo
for ResolvePath
, string
for Convert-Path
, and a type corresponding to the item retrieved for Get-Item
. These have their limitations, however:
Test-Path
, so Test-Path $_ -PathType Leaf
returning $true
is not sufficient to determine that the input is a valid file.If you want to get around these limitations, you'll have to be a bit creative. The currently accepted answer mostly accomplishes this, but fails for paths that are rooted, but not fully qualified (as an example, C:\Users
would be both rooted and fully qualified, while \Users
would be rooted but not fully qualified). To illustrate this:
PS D:\> Set-Location -Path 'C:\'
PS C:\> [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PWD, '\Users'))
D:\Users
If you're using PowerShell 6+, you can instead do:
$Path = [System.IO.Path]::GetFullPath($Path, $PWD)
$Path
isn't fully qualified, it will be resolved relative to $PWD
and normalized.$Path
is already fully qualified, $PWD
will be ignored and $Path
will be unchanged (besides normalization).$Path
doesn't exist.$Path
as a path to a file or directory (and not a registry key, for example).If you're using a PowerShell version older than 6, Santiago's answer is probably your best bet.
Now that $Path
has been resolved to a fully qualified path, System.IO
.NET methods will behave as you'd expect, allowing you to perform checks like:
[System.IO.File]::Exists($Path)
which, unlike Test-Path -Path $Path -PathType Leaf
, will ensure the path exists and is specifically a file.