2

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
lit
  • 14,456
  • 10
  • 65
  • 119

3 Answers3

2

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.

Theo
  • 57,719
  • 8
  • 24
  • 41
  • This in combination with `Set-Location` and `Push-Location` not updating the value of `[io.directory]::GetCurrentDirectory()` answered my question too, you can post it on mine too :) – Santiago Squarzon Dec 07 '21 at 14:22
  • +1 for pointing out the issue with `\ `-rooted non-UNC paths, but note that if you want to resolve them correctly you can _not_ rely on `[System.IO.Path]::GetFullPath()`, as it will again resolve relative to _.NET_'s working directory, which may be _on a different drive_. – mklement0 Dec 25 '21 at 22:32
1

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()
    }
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    This is not working for me. It seems that assigning a string, `[Io.FileInfo]$Path = '.\t.txt'` does not produce the same thing as `[Io.FileInfo]$Path = Get-ChildItem -Path '.\t.txt'`. I had to change it to `::Open((Get-ChildItem -Path $Path).FullName,...` – lit Dec 06 '21 at 19:09
  • I would be fine without .Net. My reason for using the .Net functions is speed performance. – lit Dec 06 '21 at 20:00
  • 1
    Does there need to be a test using `Split-Path`? `Resolve-Path -Path "C:\src\t\t.txt` will return a fully qualified, rooted path. – lit Dec 07 '21 at 17:33
  • 1
    I am selecting this answer since I believe just using `Resolve-Path` is the most straightforward. It uses the PowerShell (not .NET) path which is what a PowerShell developer might expect. – lit Dec 07 '21 at 20:47
  • @lit I have added a last edit to my answer in case you're interested. – Santiago Squarzon Dec 08 '21 at 00:46
0

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)
js2010
  • 23,033
  • 6
  • 64
  • 66