Private/Tools.Class.FormattedFileSystemPath.ps1
class FormattedFileSystemPath { <# .SYNOPSIS A class that receives a file system path, formats the path automatically when initialized, holds the formatted path, and provides some useful attributes(properties) simultaneously for a quick check. .NOTES Support file system paths only! .DESCRIPTION Automatically format a path to standard format by the following procedures and rules: **First**: Preprocess a received path with some literal check (string level, without accessing it by file system): - Check if it contains wildcard characters `*`, `?` or `[]`. If so, throw an error. - Check if it contains more than 1 group of consecutive colons. If so, throw an error. - Reduce any consecutive colons to a single `:` - Strip any trailing slashs. - According to the platform, append a single `\` or `/` if the path ends with a colon. - Reduce any consecutive slashes to a single one, and convert them to `\` or `/`, according to the platform. - Convert the drive name to initial capital letter. - If there are no colons in the path, or there is no slash at the beginning, it will be treated as a relative path. Then a slash `\` or `/`, according to the platform will be added at the head. **Second**: Test the preprocessed path with file system access: - Check if the path exists in file system. If not, throw an error. - Check if the path is with wildcard characters by file system. If so, throw an error. - It means a path (an instance of this class) represents only a path, not a group of paths. **Third** Format the path with file system access: - Convert it to an absolute one. - Convert it to an original-case one. - Even though, by [default](https://learn.microsoft.com/zh-cn/windows/wsl/case-sensitivity), items in NTFS of Windows is not case-sensitive, but actually it has the ability to be case-sensitive. - And, in NTFS of Windows, two paths with only case differences can represent the same item, i.g., `c:\uSeRs\usER\TesT.tXt` and `C:\Users\User\test.txt`. - Furthermore, by `explorer.exe`, we can see that the original case of a path. If we change its case, the original case will be changed too. - So, NTFS does save and maintain the original case of a path. It just be intentionally case-insensitive rather than incapable of being case-sensitive. - This class use the methods [here](https://stackoverflow.com/q/76982195/17357963) to get the original case of a path, then maintian it. **#TODO **: - Cross-platform support. - Currently, this class is only adapative on each single platform, but not cross-platform. - But for preliminary process, the source's platform will be detected and recorded in the property `OriginalPlatform`. **Some properties of the path are also provided**: 1. LiteralPath: The formatted path. 2. OriginalPlatform: The platform of the source path. 3. Slash: The slash of the path. 4. Attributes: The attributes of the path. 5. Linktype: The link type of the path. 6. LinkTarget: The link target of the path. 7. Qualifier: The qualifier of the path. 8. QualifierRoot: The root of the qualifier of the path. 9. DriveFormat: The format of the drive of the path. 10. IsDir: If the path is a directory. 11. IsFile: If the path is a file. 12. IsDriveRoot: If the path is the root of a drive. 13. IsBeOrInSystemDrive: If the path is in the system drive. 14. IsInHome: If the path is in the home directory. 15. IsHome: If the path is the home directory. 16. IsDesktopINI: If the path is a desktop.ini file. (Windows only): 17. IsSystemVolumeInfo: If the path is the System Volume Information directory. 18. IsInSystemVolumeInfo: If the path is in the System Volume Information directory. 19. IsRecycleBin: If the path is the Recycle Bin directory. 20. IsInRecycleBin: If the path is in the Recycle Bin directory. 21. IsSymbolicLink: If the path is a symbolic link. 22. IsJunction: If the path is a junction. 23. IsHardLink: If the path is a hard link. .EXAMPLE Not usage examples, but a demonstration about the path formatting: | (Windows) Existing Path | Given(Input) Path | Formatted Path | | ------------------------- | ------------------------- | ----------------- | | C:\Users | c:\uSeRs | C:\Users\ | | C:\Users | C:\uSers | C:\Users\ | | C:\Users\test.txt | c:\uSeRs\usER\TesT.tXt | C:\Users\test.txt | | C:\Users\test.txt | C:\uSeRs\user\TEST.TxT | C:\Users\test.txt | | (Unix) Existing Path | Given(Input) Path | Formatted Path | | ------------------------- | ------------------------- | ----------------- | | /home/uSer | /home/uSer | /home/uSer/ | .LINK Refer to the [default case-sensitive](https://learn.microsoft.com/zh-cn/windows/wsl/case-sensitivity). Refer to the [methods](https://stackoverflow.com/q/76982195/17357963) to get the original case of a path. #> [ValidateNotNullOrEmpty()][string] $LiteralPath [ValidateNotNullOrEmpty()][string] $OriginalPlatform [ValidateSet('\','/')][string] $Slash [AllowNull()][string] $Attributes [AllowNull()][string] $Linktype = $null [AllowNull()][string] $LinkTarget = $null [ValidateNotNullOrEmpty()][string] $Qualifier [ValidateNotNullOrEmpty()][string] $QualifierRoot [ValidateNotNullOrEmpty()][string] $DriveFormat [ValidateNotNullOrEmpty()][bool] $IsDir [ValidateNotNullOrEmpty()][bool] $IsFile [ValidateNotNullOrEmpty()][bool] $IsDriveRoot [ValidateNotNullOrEmpty()][bool] $IsBeOrInSystemDrive [ValidateNotNullOrEmpty()][bool] $IsInHome [ValidateNotNullOrEmpty()][bool] $IsHome [Nullable[bool]] $IsDesktopINI = $null [Nullable[bool]] $IsSystemVolumeInfo = $null [Nullable[bool]] $IsInSystemVolumeInfo = $null [Nullable[bool]] $IsRecycleBin = $null [Nullable[bool]] $IsInRecycleBin = $null [ValidateNotNullOrEmpty()][bool] $IsSymbolicLink [ValidateNotNullOrEmpty()][bool] $IsJunction [ValidateNotNullOrEmpty()][bool] $IsHardLink FormattedFileSystemPath([string] $Path) { if ([Environment]::OSVersion.Platform -eq "Win32NT"){ $this.OriginalPlatform = "Win32NT" $this.Slash = '\' }elseif ([Environment]::OSVersion.Platform -eq "Unix") { $this.OriginalPlatform = "Unix" $this.Slash = '/' }else{ throw "Only Win32NT and Unix are supported, not $($global:PSVersionTable.Platform)." } $Path = $this.PreProcess($Path) if(!(Test-Path -LiteralPath $Path)){ throw (New-Object System.Management.Automation.ItemNotFoundException "Path '$Path' not found.") } if ($this.GetQualifier($Path).Provider.Name -ne 'FileSystem'){ throw "Only FileSystem provider is supported, not $($this.GetQualifier($Path).Provider.Name)." } $this.LiteralPath = $this.FormatPath($Path) $this.Attributes = (Get-ItemProperty $this.LiteralPath).Attributes $this.Linktype = (Get-ItemProperty $this.LiteralPath).Linktype $link_target = (Get-ItemProperty $this.LiteralPath).LinkTarget if ($link_target){ $link_target = $this.PreProcess($link_target) $this.LinkTarget = $this.FormatPath($link_target) }else{ $this.LinkTarget = $link_target } $this.Qualifier = $this.GetQualifier($this.LiteralPath).Name $this.QualifierRoot = $this.GetQualifier($this.LiteralPath).Root $this.DriveFormat = ([System.IO.DriveInfo]::GetDrives() | Where-Object {$_.RootDirectory.FullName -eq $this.QualifierRoot}).DriveFormat if (Test-Path -LiteralPath $this.LiteralPath -PathType Container){ $this.IsDir = $true $this.IsFile = $false } else { $this.IsDir = $false $this.IsFile = $true } if ($this.LiteralPath -eq $this.QualifierRoot){ $this.IsDriveRoot = $true } else { $this.IsDriveRoot = $false } $home_path = $this.FormatPath([Environment]::GetFolderPath("UserProfile")) if ($this.Qualifier -eq $this.GetQualifier($home_path).Name){ $this.IsBeOrInSystemDrive = $true } else { $this.IsBeOrInSystemDrive = $false } if ($this.LiteralPath.StartsWith($home_path)){ if ($this.LiteralPath.EndsWith($home_path)){ $this.IsHome = $true $this.IsInHome = $false }else{ $this.IsHome = $false $this.IsInHome = $true } }else{ $this.IsHome = $false $this.IsInHome = $false } if ($this.OriginalPlatform -eq "Win32NT"){ if ($this.IsFile -and ((Split-Path $this.LiteralPath -Leaf) -eq "desktop.ini")){ $this.IsDesktopINI = $true } else { $this.IsDesktopINI = $false } $system_volume_information_path = $this.FormatPath("$($this.QualifierRoot)System Volume Information") if ($this.LiteralPath.StartsWith($system_volume_information_path)){ if ($this.LiteralPath.EndsWith($system_volume_information_path)){ $this.IsSystemVolumeInfo = $true $this.IsInSystemVolumeInfo = $false }else{ $this.IsSystemVolumeInfo = $false $this.IsInSystemVolumeInfo = $true } }else{ $this.IsSystemVolumeInfo = $false $this.IsInSystemVolumeInfo = $false } $recycle_bin_path = $this.FormatPath("$($this.QualifierRoot)`$RECYCLE.BIN") if ($this.LiteralPath.StartsWith($recycle_bin_path)){ if ($this.LiteralPath.EndsWith($recycle_bin_path)){ $this.IsRecycleBin = $true $this.IsInRecycleBin = $false }else{ $this.IsRecycleBin = $false $this.IsInRecycleBin = $true } }else{ $this.IsRecycleBin = $false $this.IsInRecycleBin = $false } } if ([bool]($this.Attributes -band [System.IO.FileAttributes]::ReparsePoint)){ if ($this.Linktype -eq 'SymbolicLink'){ $this.IsSymbolicLink = $true $this.IsJunction = $false $this.IsHardLink = $false } elseif ($this.Linktype -eq 'Junction'){ $this.IsSymbolicLink = $false $this.IsJunction = $true $this.IsHardLink = $false } else{ $this.IsSymbolicLink = $false $this.IsJunction = $false $this.IsHardLink = $false } }elseif($this.Linktype -eq 'HardLink'){ $this.IsSymbolicLink = $false $this.IsJunction = $false $this.IsHardLink = $true }else{ $this.IsSymbolicLink = $false $this.IsJunction = $false $this.IsHardLink = $false } } [string] PreProcess([string] $Path){ return [FormattedFileSystemPath]::FormatLiteralPath($Path,$this.Slash) } static [string] FormatLiteralPath([string] $Path, [string] $Slash){ # Format $Path on Literal level, without any check or validation through file system. # See .DESCRIPTION-1 of this class for the details of the formatting rules. # It can be used as pre-procession of a path before it is passed to $this.FormatPath(). if ($Path -match '[\*\?\[\]]'){ throw "Only literal path is supported, not $($Path) with wildcard characters `*`, `?` or `[]`." } if ($Path -match '(:+)([^:]+)(:+)'){ throw "The $($Path) should not contain more than 1 group of consecutive colons." } $Path = $Path -replace '[:]+', ':' $Path = $Path -replace '([^\\\/])([\\\/])+$', { $_.Groups[1].Value.ToUpper()} # Trim the end '\/' but remain the former characters. if ($Path -match ":$") { $Path = $Path + $Slash } $Path = $Path -replace '[/\\]+', $Slash $Path = $Path -replace '^([A-Za-z])([A-Za-z]*)(:)', { $_.Groups[1].Value.ToUpper() + $_.Groups[2].Value.ToLower() + $_.Groups[3].Value} if (($Path -notmatch ':') -and ($Path -match '^[A-Za-z]')){ $Path = $Slash + $Path } return $Path } [string] FormatPath([string] $Path){ try { $parent = Split-Path $Path -Parent } catch { $parent = '' } try { $leaf = Split-Path $Path -Leaf } catch { $leaf = '' } if ($parent -and $leaf){ $item = (Get-ChildItem $parent -Force| Where-Object Name -eq $leaf) }else{ $item = $null } if ($item){ return $item.FullName }else{ return $Path } } [System.Management.Automation.PSDriveInfo] GetQualifier([string]$LiteralPath){ return (Get-ItemProperty -LiteralPath $LiteralPath).PSDrive } # [string] GetQualifierWithFirstDir(){ # $splited_paths = $this.LiteralPath -split '\\' # if ($splited_paths.Count -gt 1) { $max_index = 1 } else { $max_index = 0 } # return $this.FormatPath($splited_paths[0..$max_index] -join '\\') # } [string] ToString() { # like __repr__ in python return $this.LiteralPath } [string] ToShortName() { return ($this.LiteralPath -replace '[/\\:]+', '-').Trim('-') } } function Get-FormattedFileSystemPath{ <# .DESCRIPTION A function to apply the class FormattedFileSystemPath on a path. Return an instance of it #> param( [Parameter(Mandatory)] [string]$Path ) return [FormattedFileSystemPath]::new($Path) } |