Private/PathTools.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: 1. 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. 2. 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. 3. 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) Existent 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) Existent Path | Given(Input) Path | Formatted Path | | ------------------------- | ------------------------- | ----------------- | | /home/uSer | /home/uSer | /home/uSer/ | #> [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 ([System.Environment]::OSVersion.Platform -eq "Win32NT"){ $this.OriginalPlatform = "Win32NT" $this.Slash = '\' }elseif ([System.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([System.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 Format-FileSystemPath{ <# .DESCRIPTION A function to apply the class FormattedFileSystemPath on a path. Return the formatted liiteral path. #> param( [Parameter(Mandatory)] [string]$Path ) return ([FormattedFileSystemPath]::new($Path)).LiteralPath } class EnvPaths{ <# .SYNOPSIS A class that maintains the process, user, and machine level env paths, holds the de-duplicated paths, and provides some useful methods for some scenarios that need to modify the env paths. .NOTES Do not check any path's existence or validity. #> [ValidateNotNullOrEmpty()][string] $OriginalPlatform [ValidateNotNullOrEmpty()][string] $Indicator [ValidateNotNullOrEmpty()][string] $Separator [ValidateNotNullOrEmpty()][string[]] $ProcessLevelEnvPaths [AllowNull()][string[]] $UserLevelEnvPaths [AllowNull()][string[]] $MachineLevelEnvPaths [ValidateNotNullOrEmpty()][string[]] $DeDuplicatedProcessLevelEnvPaths [AllowNull()][string[]] $DeDuplicatedUserLevelEnvPaths [AllowNull()][string[]] $DeDuplicatedMachineLevelEnvPaths EnvPaths() { if ([System.Environment]::OSVersion.Platform -eq "Win32NT"){ $this.OriginalPlatform = "Win32NT" $this.Indicator = 'Path' $this.Separator = ';' }elseif ([System.Environment]::OSVersion.Platform -eq "Unix") { $this.OriginalPlatform = "Unix" $this.Indicator = 'PATH' $this.Separator = ':' }else{ throw "Only Win32NT and Unix are supported, not $($global:PSVersionTable.Platform)." } $this.ProcessLevelEnvPaths = @([Environment]::GetEnvironmentVariable($this.Indicator,'Process') -Split $this.Separator) $this.UserLevelEnvPaths = @([Environment]::GetEnvironmentVariable($this.Indicator,'User') -Split $this.Separator) $this.MachineLevelEnvPaths = @([Environment]::GetEnvironmentVariable($this.Indicator,'Machine') -Split $this.Separator) $this.ProcessLevelEnvPaths = $this.DeEmpty($this.ProcessLevelEnvPaths) $this.UserLevelEnvPaths = $this.DeEmpty($this.UserLevelEnvPaths) $this.MachineLevelEnvPaths = $this.DeEmpty($this.MachineLevelEnvPaths) if ($this.OriginalPlatform -eq "Unix"){ if ($this.UserLevelEnvPaths.Count -ne 0){ throw "In Unix platform, the User level env path should be empty. But it is $($this.UserLevelEnvPaths)." } if ($this.MachineLevelEnvPaths.Count -ne 0){ throw "In Unix platform, the Machine level env path should be empty. But it is $($this.MachineLevelEnvPaths)." } } $verbose = $false $this.DeDuplicatedProcessLevelEnvPaths = $this.DeDuplicate($this.ProcessLevelEnvPaths,'Process',$verbose) $this.DeDuplicatedUserLevelEnvPaths = $this.DeDuplicate($this.UserLevelEnvPaths,'Process',$verbose) $this.DeDuplicatedMachineLevelEnvPaths = $this.DeDuplicate($this.MachineLevelEnvPaths,'Process',$verbose) } [void] FindDuplicatedPaths([string[]] $Paths, [string] $Level,[bool]$Verbose){ $grouped_paths = $Paths | Group-Object $duplicated_groups = $grouped_paths | Where-Object { $_.Count -gt 1 } if ($Verbose){ foreach ($group in $duplicated_groups) { Write-Logs "[Env Paths Duplicated] The $($group.Name) in '$Level' level env path exists $($group.Count) times." -ShowVerbose } }else{ foreach ($group in $duplicated_groups) { Write-Logs "[Env Paths Duplicated] The $($group.Name) in '$Level' level env path exists $($group.Count) times." } } } [string[]] DeEmpty([string[]] $Paths){ $buf = @() foreach ($item in $Paths) { if ($item.Trim()){ $buf += $item } } return $buf } [string[]] DeDuplicate([string[]] $Paths, [string] $Level,[bool]$Verbose){ $this.FindDuplicatedPaths($Paths,$Level,$Verbose) $buf = @() foreach ($item in $Paths) { if (-not $buf.Contains($item)){ $buf += $item } } return $buf } [void] SetEnvPath([string[]] $Paths, [string] $Level){ [Environment]::SetEnvironmentVariable($this.Indicator,$Paths -join $this.Separator,$Level) } [void] DeDuplicateProcessLevelEnvPaths(){ $verbose = $true $this.ProcessLevelEnvPaths = $this.DeDuplicate($this.ProcessLevelEnvPaths,'Process',$verbose) $this.SetEnvPath($this.ProcessLevelEnvPaths,'Process') Write-Logs "[Env Paths Modifed] The 'Process' level env path has been de-duplicated." -ShowVerbose } [void] DeDuplicateUserLevelEnvPaths(){ $verbose = $true $this.UserLevelEnvPaths = $this.DeDuplicate($this.UserLevelEnvPaths,'User',$verbose) $this.SetEnvPath($this.UserLevelEnvPaths,'User') Write-Logs "[Env Paths Modifed] The 'User' level env path has been de-duplicated." -ShowVerbose } [void] DeDuplicateMachineLevelEnvPaths(){ $verbose = $true $this.MachineLevelEnvPaths = $this.DeDuplicate($this.MachineLevelEnvPaths,'Machine',$verbose) $this.SetEnvPath($this.MachineLevelEnvPaths,'Machine') Write-Logs "[Env Paths Modifed] The 'Machine' level env path has been de-duplicated." -ShowVerbose } [void] MergeDeDuplicatedEnvPathsFromMachineLevelToUserLevel(){ $this.DeDuplicateUserLevelEnvPaths() $this.DeDuplicateMachineLevelEnvPaths() $buf = $this.UserLevelEnvPaths+$this.MachineLevelEnvPaths $verbose = $true $this.FindDuplicatedPaths($buf,'User+Machine',$verbose) $buf = @() foreach ($item in $this.MachineLevelEnvPaths) { if (-not $this.UserLevelEnvPaths.Contains($item)){ $buf += $item } } $this.MachineLevelEnvPaths = $buf $this.SetEnvPath($this.MachineLevelEnvPaths,'Machine') Write-Logs "[Env Paths Modifed] The items duplicated across 'Machine' level and 'User' level env path have been merged into 'User' level env path." -ShowVerbose } [string[]] Append([string[]] $Paths, [string] $Level,[string] $Path){ $buf = $Paths.Clone() if (-not $buf.Contains($Path)){ $buf += $Path }else{ Write-Logs "[Env Paths Duplicated] The $Path in '$Level' level is existent already." -ShowVerbose } return $buf } [void] AppendProcessLevelEnvPaths([string] $Path){ $this.DeDuplicateProcessLevelEnvPaths() $this.ProcessLevelEnvPaths = $this.Append($this.ProcessLevelEnvPaths,'Process',$Path) $this.SetEnvPath($this.ProcessLevelEnvPaths,'Process') Write-Logs "[Env Paths Modifed] The $Path has been appended into 'Process' level env path." -ShowVerbose } [void] AppendUserLevelEnvPaths([string] $Path){ $this.DeDuplicateUserLevelEnvPaths() $this.UserLevelEnvPaths = $this.Append($this.UserLevelEnvPaths,'User',$Path) $this.SetEnvPath($this.UserLevelEnvPaths,'User') Write-Logs "[Env Paths Modifed] The $Path has been appended into 'User' level env path." -ShowVerbose } [void] AppendMachineLevelEnvPaths([string] $Path){ $this.DeDuplicateMachineLevelEnvPaths() $this.MachineLevelEnvPaths = $this.Append($this.MachineLevelEnvPaths,'Machine',$Path) $this.SetEnvPath($this.MachineLevelEnvPaths,'Machine') Write-Logs "[Env Paths Modifed] The $Path has been appended into 'Machine' level env path." -ShowVerbose } [string[]] Remove([string[]] $Paths, [string] $Level, [string] $Path, [bool] $IsPattern){ $buf = @() foreach ($item in $Paths) { if ($IsPattern){ if ($item -NotMatch $Path){ $buf += $item }else{ Write-Logs "[Env Paths to Remove] The $item in '$Level' level will be removed." -ShowVerbose } }else{ if ($item -ne $Path){ $buf += $item }else{ Write-Logs "[Env Paths to Remove] The $item in '$Level' level will be removed." -ShowVerbose } } } return $buf } [void] RemoveProcessLevelEnvPaths([string] $Target, [bool] $IsPattern){ $this.DeDuplicateProcessLevelEnvPaths() $this.ProcessLevelEnvPaths = $this.Remove($this.ProcessLevelEnvPaths,'Process',$Target,$IsPattern) $this.SetEnvPath($this.ProcessLevelEnvPaths,'Process') Write-Logs "[Env Paths Modifed] The removement has been done on 'Process' level env path." -ShowVerbose } [void] RemoveUserLevelEnvPaths([string] $Target, [bool] $IsPattern){ $this.DeDuplicateUserLevelEnvPaths() $this.UserLevelEnvPaths = $this.Remove($this.UserLevelEnvPaths,'User',$Target,$IsPattern) $this.SetEnvPath($this.UserLevelEnvPaths,'User') Write-Logs "[Env Paths Modifed] The removement has been done on 'User' level env path." -ShowVerbose } [void] RemoveMachineLevelEnvPaths([string] $Target, [bool] $IsPattern){ $this.DeDuplicateMachineLevelEnvPaths() $this.MachineLevelEnvPaths = $this.Remove($this.MachineLevelEnvPaths,'Machine',$Target,$IsPattern) $this.SetEnvPath($this.MachineLevelEnvPaths,'Machine') Write-Logs "[Env Paths Modifed] The removement has been done on 'Machine' level env path." -ShowVerbose } } function Get-EnvPaths{ <# .DESCRIPTION A function to apply the class EnvPaths. Return the formatted liiteral path. #> param() return [EnvPaths]::new() } |