powershell Get-Content and .Net Open() handing path differently

Viewed 122

Get-Content appears to use the current working directory location to resolve realative paths. However, the .Net System.Io.File Open() method does not. What is the PowerShell-centric way to resolve a relative path for .Net?

PS C:\src\t> type .\ReadWays.ps1
[CmdletBinding()]
param (
    [Parameter(Mandatory=$true)]
    [String]$Path
)
Write-Host "Path is $Path"
Get-Content -Path $Path | Out-Null
if ([System.IO.StreamReader]$sr = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open)) { $sr.Close() }

PS C:\src\t> .\ReadWays.ps1 -Path '.\t.txt'
Path is .\t.txt
MethodInvocationException: C:\src\t\ReadWays.ps1:8
Line |
   8 |  if ([System.IO.StreamReader]$sr = [System.IO.File]::Open($Path, [Syst …
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exception calling "Open" with "2" argument(s): "Could not find file 'C:\Program Files\PowerShell\7\t.txt'."

PS C:\src\t> $PSVersionTable.PSVersion.ToString()
7.2.0
3 Answers

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

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.

The following works fine for me and is compatible with Windows and Linux. This is using Convert-Path to resolve the relative paths. I was previously using Resolve-Path which is incorrect, only the former resolves to file-system-native paths, thanks mklement0 for pointing it out

param(
    [ValidateScript({ 
        if(Test-Path $_ -PathType Leaf)
        {
            return $true
        }
        throw 'Invalid File Path'
    })]
    [string]$Path
)

if(-not $Path.StartsWith('\\'))
{
    [string]$Path = Convert-Path $Path
}

$reader = [System.IO.StreamReader]::new(
    [System.IO.File]::Open(
        $Path, [System.IO.FileMode]::Open
    )
)

$reader.BaseStream
$reader.Close()

Last Edit

The following should be able to handle:

  • UNC Paths
  • Work on Windows and Linux
  • Be efficient
  • Handle Relative Paths

Starting from the base that $Path is valid thanks to the ValidateScript attribute, we only need to determine if the path we are dealing with is UNC, Relative or Absolute.

UNC paths must always be fully qualified. They can include relative directory segments (. and ..), but these must be part of a fully qualified path. You can use relative paths only by mapping a UNC path to a drive letter.

We can assume a UNC path must always start with \\, so this condition should suffice to determine if $Path will be manipulated or not:

if(-not $Path.StartsWith('\\'))

Lastly, in the begin block, updating the environment's current directory each time our script or function runs with:

[Environment]::CurrentDirectory = $pwd.ProviderPath

By doing so, ([System.IO.FileInfo]$Path).FullName should give us the absolute path of our parameter, be it UNC, Relative or Absolute.

param(
    [ValidateScript({ 
        if(Test-Path $_ -PathType Leaf) {
            return $true
        }
        throw 'Invalid File Path'
    })] [string]$Path
)

begin
{
    [Environment]::CurrentDirectory = $pwd.ProviderPath
}

process
{
    if(-not $Path.StartsWith('\\'))
    {
        $Path = ([System.IO.FileInfo]$Path).FullName
    }

    try
    {
        $reader = [System.IO.StreamReader]::new(
            [System.IO.File]::Open(
                $Path, [System.IO.FileMode]::Open
            )
        )

        $reader.BaseStream
    }
    catch
    {
        $_.Exception.Message
    }
    finally
    {
        $reader.Close()
        $reader.Dispose()
    }
}

This is a common question. Somehow .net and powershell don't agree on the current directory.

[System.IO.File]::Open("$pwd\$Path", [System.IO.FileMode]::Open)
Related