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
}
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))
}
}
}
Another option:
As you wanted the result of a cast to [System.IO.FileInfo], you can instead use Get-Item, which will also return a [System.IO.FileInfo] object, but with resolved relative paths as expected. It will also incorporate some error detection (invalid characters or non-existent path etc.).
Example:
PS C:\Users\User\Documents> (Get-Item -LiteralPath '.\test.txt').FullName
C:\Users\User\Documents\test.txt
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
Related
This question already has answers here:
How does PowerShell treat "." in paths?
(3 answers)
Closed 1 year ago.
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
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)
Consider the following situation: I have a parameter, or a config variable, that sets the output directory of a script. Obviously, this parameter should also be able to be absolute:
RepoBackup.ps1 -OutputDirectory .\out
RepoBackup.ps1 -OutputDirectory D:\backup
In the script, I use (Get-Item -Path './').FullName in combination with Join-Path to determine the absolute path of my output directory, because I might need to use Set-Location to change the current directory - which makes working with relative paths complicated.
But:
Join-Path C:\code\ .\out # => C:\code\.\out (which is exactly what i need)
Join-Path C:\code\ D:\ # => C:\code\D:\ (which is not only not what i need, but invalid)
I considered using Resolve-Path and do something like Resolve-Path D:\backup, but if the directory doesn't exist (yet), this yields an error about not being able to find the path.
So, how can I get the absolute path of my $OutputDirectory, accepting both absolute and relative input, as well as paths that don't yet exist?
This function did the job for me:
function Join-PathOrAbsolute ($Path, $ChildPath) {
if (Split-Path $ChildPath -IsAbsolute) {
Write-Verbose ("Not joining '$Path' with '$ChildPath'; " +
"returning the child path as it is absolute.")
$ChildPath
} else {
Write-Verbose ("Joining path '$Path' with '$ChildPath', " +
"child path is not absolute")
Join-Path $Path $ChildPath
}
}
# short version, without verbose messages:
function Join-PathOrAbsolute ($Path, $ChildPath) {
if (Split-Path $ChildPath -IsAbsolute) { $ChildPath }
else { Join-Path $Path $ChildPath }
}
Join-PathOrAbsolute C:\code .\out # => C:\code\.\out (just the Join-Path output)
Join-PathOrAbsolute C:\code\ D:\ # => D:\ (just the $ChildPath as it is absolute)
It just checks whether the latter path is absolute and returns it if it is, otherwise it just runs Join-Path on both $Path and $ChildPath.
Note that this doesn't consider the base $Path to be relative, but for my use case this is perfectly enough. (I use (Get-Item -Path './').FullName as the base path, which is absolute anyway.)
Join-PathOrAbsolute .\ D:\ # => D:\
Join-PathOrAbsolute .\ .\out # => .\.\out
Note that while .\.\ and C:\code\.\out does indeed look weird, it is valid and resolves to the correct path. It's just the output of PowerShell's integrated Join-Path function, after all.
You can use the .Net Path.Combine method, which does exactly what you want.
[IO.Path]::Combine('C:\code\', '.\out') # => 'C:\code\.\out'
[IO.Path]::Combine('C:\code\', 'D:\') # => 'D:\'
I am automatically obtaining directories from an application but I can't seem to get the actual directories with the correct case of letters.
For example I get $a='C:\test\dir\log\wqerst' but the actual directory is C:\test\dir\log\WQERST.
What I want is to uppercase only wqerst so it would show C:\test\dir\log\WQERST
I've already tried using substring but I don't know how I would be able to connect it to the whole directory once it is uppercase.
Windows system is not case sensitive, but if you want really this result, you can do it :
$a='C:\test\dir\log\wqerst'
$parentpath=Split-Path -Path $a
$file=(Split-Path -Path $a -Leaf).ToUpper()
$result=Join-Path $parentpath $file
$result
As James C. and vonPryz already wrote, there is not much point to get the case sensitive folder path. However you can use this helper method:
function Get-CaseSensitiveFilePath
{
Param
(
[string]$FilePath
)
$parent = Split-Path $FilePath
$leaf = Split-Path -Leaf $FilePath
$result = Get-ChildItem $parent | where { $_ -like $leaf }
$result.FullName
}
usage:
Get-CaseSensitiveFilePath -FilePath 'C:\test\dir\log\WQERST'
This will give you the case sensitive folder name but the directory must exist on the computer you execute the script...
I'm trying to process a list of files that may or may not be up to date and may or may not yet exist. In doing so, I need to resolve the full path of an item, even though the item may be specified with relative paths. However, Resolve-Path prints an error when used with a non-existant file.
For example, What's the simplest, cleanest way to resolve ".\newdir\newfile.txt" to "C:\Current\Working\Directory\newdir\newfile.txt" in Powershell?
Note that System.IO.Path's static method use with the process's working directory - which isn't the powershell current location.
You want:
c:\path\exists\> $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(".\nonexist\foo.txt")
returns:
c:\path\exists\nonexists\foo.txt
This has the advantage of working with PSPaths, not native filesystem paths. A PSPath may not map 1-1 to a filesystem path, for example if you mount a psdrive with a multi-letter drive name.
What's a pspath?
ps c:\> new-psdrive temp filesystem c:\temp
...
ps c:\> cd temp:
ps temp:\>
temp:\ is a drive-qualified pspath that maps to a win32 (native) path of c:\temp.
-Oisin
When Resolve-Path fails due to the file not existing, the fully resolved path is accessible from the thrown error object.
You can use a function like the following to fix Resolve-Path and make it work like you expect.
function Force-Resolve-Path {
<#
.SYNOPSIS
Calls Resolve-Path but works for files that don't exist.
.REMARKS
From http://devhawk.net/blog/2010/1/22/fixing-powershells-busted-resolve-path-cmdlet
#>
param (
[string] $FileName
)
$FileName = Resolve-Path $FileName -ErrorAction SilentlyContinue `
-ErrorVariable _frperror
if (-not($FileName)) {
$FileName = $_frperror[0].TargetObject
}
return $FileName
}
I think you're on the right path. Just use [Environment]::CurrentDirectory to set .NET's notion of the process's current dir e.g.:
[Environment]::CurrentDirectory = $pwd
[IO.Path]::GetFullPath(".\xyz")
Join-Path (Resolve-Path .) newdir\newfile.txt
This has the advantage of not having to set the CLR Environment's current directory:
[IO.Path]::Combine($pwd,"non\existing\path")
NOTE
This is not functionally equivalent to x0n's answer. System.IO.Path.Combine only combines string path segments. Its main utility is keeping the developer from having to worry about slashes. GetUnresolvedProviderPathFromPSPath will traverse the input path relative to the present working directory, according to the .'s and ..'s.
I've found that the following works well enough.
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$newFile = "newDir\newFile.txt"
Do-Something-With "$workingDirectory\$newFile"
Convert-Path can be used to get the path as a string, although this is not always the case. See this entry on COnvert-Path for more details.
function Get-FullName()
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline = $True)] [object[]] $Path
)
Begin{
$Path = #($Path);
}
Process{
foreach($p in $Path)
{
if($p -eq $null -or $p -match '^\s*$'){$p = [IO.Path]::GetFullPath(".");}
elseif($p -is [System.IO.FileInfo]){$p = $p.FullName;}
else{$p = [IO.Path]::GetFullPath($p);}
$p;
}
}
}
I ended up with this code in my case. I needed to create a file later in the the script, so this code presumes you have write access to the target folder.
$File = ".\newdir\newfile.txt"
If (Test-Path $File) {
$Resolved = (Resolve-Path $File).Path
} else {
New-Item $File -ItemType File | Out-Null
$Resolved = (Resolve-Path $File).Path
Remove-Item $File
}
I also enclosed New-Item in try..catch block, but that goes out of this question.
I had a similar issue where I needed to find the folder 3 levels up from a folder that does not exist yet to determine the name for a new folder I wanted to create... It's complicated. Anyway, this is what I ended up doing:
($path -split "\\" | select -SkipLast 3) -join "\\"
You can just set the -errorAction to "SilentlyContinue" and use Resolve-Path
5 > (Resolve-Path .\AllFilerData.xml -ea 0).Path
C:\Users\Andy.Schneider\Documents\WindowsPowerShell\Scripts\AllFilerData.xml
6 > (Resolve-Path .\DoesNotExist -ea 0).Path
7 >
There is an accepted answer here, but it is quite lengthy and there is a simpler alternative available.
In any recent version of Powershell, you can use Test-Path -IsValid -Path 'C:\Probably Fake\Path.txt'
This simply verifies that there are no illegal characters in the path and that the path could be used to store a file. If the target doesn't exist, Test-Path won't care in this instance -- it's only being asked to test if the provided path is potentially valid.
Both most popular answers don't work correctly on paths on not existing drives.
function NormalizePath($filename)
{
$filename += '\'
$filename = $filename -replace '\\(\.?\\)+','\'
while ($filename -match '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\') {
$filename = $filename -replace '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\','\'
}
return $filename.TrimEnd('\')
}
Check if the file exists before resolving:
if(Test-Path .\newdir\newfile.txt) { (Resolve-Path .\newdir\newfile.txt).Path }
Well I've struggled long enough with this one. I have a project to compare two folders, one on each of two servers. We are comparing files on the source server with those on the target server and will create a list of the files from the source that will need to be refreshed once an update is completed on the target server.
Here's my script (many thanks to http://quickanddirtyscripting.wordpress.com for the original) :
param ([string] $src,[string] $dst)
function get-DirHash()
{
begin
{
$ErrorActionPreference = "silentlycontinue"
}
process
{
dir -Recurse $_ | where { $_.PsIsContainer -eq $false -and ($_.Name -like "*.js" -or $_.Name -like "*.css"} | select Name,FullName,#{Name="SHA1 Hash"; Expression={get-hash $_.FullName -algorithm "sha1" }}
}
end
{
}
}
function get-hash
{
param([string] $file = $(throw 'a filename is required'),[string] $algorithm = 'sha256')
try
{
$fileStream = [system.io.file]::openread((resolve-path $file));
$hasher = [System.Security.Cryptography.HashAlgorithm]::create($algorithm);
$hash = $hasher.ComputeHash($fileStream);
$fileStream.Close();
}
catch
{
write-host $_
}
return $hash
}
Compare-Object $($src | get-DirHash) $($dst | get-DirHash) -property #("Name", "SHA1 Hash")
Now for some reason if I run this against local paths say c:\temp\test1 c:\temp\test2 it works fine, but when I run it using UNC paths between two servers I get
Exception calling "OpenRead" with "1" argument(s): "The given path's format is not supported."
Any help with this would be greatly appreciated. The end result should be a list of files, but for some reason it doesn't like the UNC path.
The script name is compare_js_css.ps1 and is called as such:
.\compare_js_css.ps1 c:\temp\test1 c:\temp\test2 <-- This works
.\compare_js_css.ps1 \\\\devserver1\c$\websites\site1\website \\\\devserver2\c$\websites\site1\website <-- Returns the aforementioned exception.
Why?
This gives the path you are after without the Microsoft.PowerShell.Core\FileSystem:::
(Resolve-Path $file).ProviderPath
No need to use a string replace.
OpenRead supports UNC paths. Resolve-Path returns you an object. Use (Resolve-Path MyFile.txt).Path.Replace('Microsoft.PowerShell.Core\FileSystem::', '') as the argument for OpenRead. The path returned from Resolve-Path when using UNC paths includes PowerShell's fully qualified schema which contains a header which is unsupported by the OpenRead method so it needs to be omitted.
Use the Convert-Path cmdlet, which will provide you with the path in the 'regular' UNC form. This will be required any time you use any shell commands, or need to pass an entire path to a .Net method etc...
See https://technet.microsoft.com/en-us/library/ee156816.aspx