3

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.

Example

  • On Linux
PS /home/user/Documents> ([System.IO.FileInfo]'./test.txt').FullName
/home/user/Documents/test.txt
  • On Windows
PS C:\Users\User\Documents> ([System.IO.FileInfo]'.\test.txt').FullName
C:\Users\User\test.txt

EDIT

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 [System.IO.Directory]::GetCurrentDirectory() not being updated by Push-Location or Set-Location.

A simple example:

PS /home/user> [System.IO.Directory]::GetCurrentDirectory()
/home/user
PS /home/user> cd ./Documents/
PS /home/user/Documents> [System.IO.Directory]::GetCurrentDirectory()
/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
}
Santiago Squarzon
  • 20,988
  • 4
  • 10
  • 27
  • 1
    You could try `[System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, '.\test.txt'))` – Theo Dec 07 '21 at 12:10
  • @Theo that's excellent and works fine on both OS. Would you ask `[IO.Path]::IsRooted(..)` before or just do it no matter what? I guess I wont find an answer on the behavior of `FileInfo` on Windows vs Linux so you can propose this as an answer and if by the end of the day nobody can answer the other question I'll go ahead an accept it. Thanks Theo. – Santiago Squarzon Dec 07 '21 at 13:40
  • 1
    This has been discussed before. For example: https://stackoverflow.com/questions/11246068/why-dont-net-objects-in-powershell-use-the-current-directory – js2010 Dec 07 '21 at 16:36
  • @js2010 thank you! – Santiago Squarzon Dec 07 '21 at 18:33

2 Answers2

1

Feels a bit duplicate, but since you asked..

I'm sorry I don't know about Linux, but in Windows:

You can add a test first to see if the path is relative and if so, convert it to absolute like:

$Path = '.\test.txt'
if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
    $Path =  [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
}

I added $Path -match '^\\[^\\]+' to also convert relative paths starting with a backslash like \ReadWays.ps1 meaning the path starts at the root directory. UNC paths that start with two backslashes are regarded as absolute.


Apparently (I really have no idea why..) the above does not work on Linux, because there, when using a UNC path, the part ![System.IO.Path]::IsPathRooted('\\server\folder') yields True.

It seems then you need to check the OS first and do the check differently on Linux.

$Path = '\\server\share'

if ($IsWindows) {  # $IsWindows exists in version 7.x. Older versions do `$env:OS -match 'Windows'`
    if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
        $Path =  [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
    }
}
else {
    if ($Path -notlike '\\*\*') {  # exclude UNC paths as they are not relative
        if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
            $Path =  [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
        }
    }
}
Theo
  • 49,970
  • 8
  • 20
  • 38
0

The easiest alternative would be to use Convert-Path to:

  • Handle UNC, Relative, Absolute and Rooted Paths.
  • Be compatible with Windows and Linux
  • Be efficient

Another neat option if we are using [cmdletbinding()] is to use $PSCmdlet.GetUnresolvedProviderPathFromPSPath(..) method:

function ResolvePath {
    [cmdletbinding()]
    param($path)
    $PSCmdlet.GetUnresolvedProviderPathFromPSPath($path)
}

ResolvePath \\server01\test         # => \\server01\test
ResolvePath C:\Users\user\Documents # => C:\Users\user\Documents
ResolvePath C:Documents             # => C:\Documents
(ResolvePath .) -eq $PWD.Path       # => True
(ResolvePath ~) -eq $HOME           # => True
Santiago Squarzon
  • 20,988
  • 4
  • 10
  • 27