Private/Show-TreeInternal.ps1
|
function Show-TreeInternal { [CmdletBinding()] param ( [string]$Path, [switch]$Tree, [switch]$List, [int]$MaxDepth, [switch]$Colorize, [switch]$IncludeFiles, [switch]$Gap, [switch]$Ascii, [int]$CurrentDepth, [string]$Prefix, [bool]$IsLastParent ) # ANSI color codes $esc = [char]27 $colorReset = $Colorize ? "${esc}[0m" : "" # Color Reset $colorFile = $Colorize ? "${esc}[97m" : "" # Bright White $colorDir = $Colorize ? "${esc}[96m" : "" # Bright Cyan $colorConnector = $Colorize ? "${esc}[90m" : "" # Dim Gray $colorGap = $colorConnector if ($CurrentDepth -eq 0) { $Path = Get-NormalizedPath -Path $Path $NearestExistingParent = Get-NearestExistingParent -Path $Path if (-not (Test-Path $Path)) { $invalidPath = $true } if ($Tree) { $fileSystemLabel = Get-VolumeName -Path $NearestExistingParent $serialNumber = Get-VolumeSerialNumber -Path $NearestExistingParent Write-Output "Folder PATH listing for volume $fileSystemLabel" Write-Output "Volume serial number is $serialNumber" } Write-Output "${colorDir}$Path${colorReset}" if ($invalidPath) { Write-Output "Invalid path - \$($Path -Split '\\' | Select-Object -Last 1)" } } if ($MaxDepth -ne -1 -and $CurrentDepth -ge $MaxDepth) { return } if ($Tree) { $raw = Get-RawDirectoryEntries -Path $Path $dirs = $raw.Directories $files = $IncludeFiles ? $raw.Files : @() } else { $dirs = Get-ChildItem -Path $Path -Directory -ErrorAction SilentlyContinue $files = if ($IncludeFiles) { Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue } else { @() } } $fileCount = $files.Count $dirCount = $dirs.Count $noSpan = $false if ($Tree -and $dirCount -eq 0) { $noSpan = $true } # Print files first for ($j = 0; $j -lt $fileCount; $j++) { $file = $files[$j] $isLastFile = ($j -eq $fileCount - 1) -and ($dirCount -eq 0) $fileConnector = $List ? " " : $noSpan ? " " : $Tree ? $isLastFile ? # Tree $Ascii ? "| " : "│ " : $Ascii ? "| " : "│ " : $isLastFile ? # Show-Tree $Ascii ? "\-- " : "╙── " : $Ascii ? "+-- " : "╟── " Write-Output "${colorGap}${Prefix}${colorConnector}${fileConnector}${colorFile}$($file.Name)${colorReset}" } if ($invalidPath -or $CurrentDepth -eq 0 -and $dirs.Count -eq 0) { if ($Tree) { if (-not $invalidPath) { Write-Output "" } Write-Output "No subfolders exist" } Write-Output "" return } # Add a visual gap only if there are both files and directories if ($Gap -and $IncludeFiles -and $fileCount -gt 0 -and $dirCount -gt 0) { $gapConnector = $Tree ? $Ascii ? # Tree "|" : "│" : $Ascii ? # Show-Tree "|" : "║" Write-Output "${colorGap}${Prefix}${gapConnector}${colorReset}" } elseif ($Gap -and $IncludeFiles -and $fileCount -gt 0 -and $dirCount -eq 0) { Write-Output "${colorGap}${Prefix}${colorReset}" } # Print directories for ($i = 0; $i -lt $dirCount; $i++) { $dir = $dirs[$i] $isLastDir = ($i -eq $dirCount - 1) $dirConnector = $List ? " " : $Tree ? $isLastDir ? # Tree $Ascii ? "\---" : "└───" : $Ascii ? "+---" : "├───" : $isLastDir ? # Show-Tree $Ascii ? "\== " : "╚══ " : $Ascii ? "+== " : "╠══ " Write-Output "${colorGap}${Prefix}${colorConnector}${dirConnector}${colorDir}$($dir.Name)${colorReset}" $newPrefix = $Prefix + ($List ? " " : $Tree ? $isLastDir ? # Tree " " : $Ascii ? "| " : "│ " : $isLastDir ? # Show-Tree " " : $Ascii ? "| " : "║ ") # Recursively show contents $params = @{ Path = $dir.FullName Tree = $Tree List = $List IncludeFiles = $IncludeFiles Colorize = $Colorize Gap = $Gap MaxDepth = $MaxDepth CurrentDepth = $CurrentDepth + 1 Prefix = $newPrefix IsLastParent = $isLastDir Ascii = $Ascii } Show-TreeInternal @params # Add a gap only if this is the last directory and its parent is not also the last if (-not $Tree -and $Gap -and $IncludeFiles -and $isLastDir -and -not $IsLastParent -and $CurrentDepth -gt 0) { Write-Output "${colorGap}${Prefix}${colorReset}" } } # Add a final newline only after the top-level call completes if ($CurrentDepth -eq 0) { Write-Output "" } } # Normalize path to match actual casing on filesystem function Get-NormalizedPath { param ( [string]$Path = "." ) $absPath = [System.IO.Path]::GetFullPath($Path) # Remove trailing slash unless it's a root if ($absPath.Length -gt 3 -and $absPath.EndsWith("\")) { $absPath = $absPath.TrimEnd('\') } $segments = $absPath -split '\\' $normalized = @() $current = $segments[0] + "\" $normalized += $segments[0] for ($i = 1; $i -lt $segments.Count; $i++) { $segment = $segments[$i] try { $entries = Get-ChildItem -LiteralPath $current | Select-Object -ExpandProperty Name $match = $entries | Where-Object { $_.ToLower() -eq $segment.ToLower() } if ($match) { $normalized += $match $current = Join-Path $current $match } else { $normalized += $segment $current = Join-Path $current $segment } } catch { $normalized += $segment $current = Join-Path $current $segment } } return ($normalized -join '\') } function Get-NearestExistingParent { param ( [string]$Path ) $current = [System.IO.Path]::GetFullPath($Path) while (-not (Test-Path $current)) { $parent = [System.IO.Directory]::GetParent($current) if ($null -eq $parent) { return $null # Reached the root and nothing exists } $current = $parent.FullName } return $current } function Get-VolumeName { param ( [string]$Path = "." ) $driveLetter = (Get-Item $Path).PSDrive.Name $volume = Get-Volume -DriveLetter $driveLetter return $volume.FileSystemLabel } # Get volume serial number function Get-VolumeSerialNumber { param ( [string]$Path = "." ) # --- Win32 GetVolumeInformation (all filesystems) --- if (-not ([System.Management.Automation.PSTypeName]'VolumeInfo').Type) { $definition = @" using System; using System.Runtime.InteropServices; public class VolumeInfo { [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern bool GetVolumeInformation( string lpRootPathName, System.Text.StringBuilder lpVolumeNameBuffer, int nVolumeNameSize, out uint lpVolumeSerialNumber, out uint lpMaximumComponentLength, out uint lpFileSystemFlags, System.Text.StringBuilder lpFileSystemNameBuffer, int nFileSystemNameSize); } "@ Add-Type -TypeDefinition $definition -ErrorAction SilentlyContinue | Out-Null } $root = [System.IO.Path]::GetPathRoot((Resolve-Path $Path).Path) $serial = 0 $null1 = 0 $null2 = 0 $volName = New-Object System.Text.StringBuilder 261 $fsName = New-Object System.Text.StringBuilder 261 [VolumeInfo]::GetVolumeInformation( $root, $volName, $volName.Capacity, [ref]$serial, [ref]$null1, [ref]$null2, $fsName, $fsName.Capacity ) | Out-Null $serialHigh = ($serial -shr 16) $serialLow = ($serial -band 0xFFFF) return "{0:X4}-{1:X4}" -f $serialHigh, $serialLow } # Get the Directory Entires via Win32 calls directly function Get-RawDirectoryEntries { param([string]$Path) if (-not ([System.Management.Automation.PSTypeName]'RawEnum').Type) { $definition = @" using System; using System.IO; using System.Runtime.InteropServices; using System.Collections.Generic; public class RawEnum { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WIN32_FIND_DATA { public uint dwFileAttributes; public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; public uint dwReserved0; public uint dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string cFileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll")] static extern bool FindClose(IntPtr hFindFile); public static IEnumerable<WIN32_FIND_DATA> Enum(string path) { WIN32_FIND_DATA data; IntPtr handle = FindFirstFile(Path.Combine(path, "*"), out data); if (handle == new IntPtr(-1)) yield break; do { string name = data.cFileName; if (name != "." && name != "..") yield return data; } while (FindNextFile(handle, out data)); FindClose(handle); } } "@ Add-Type -TypeDefinition $definition -ErrorAction SilentlyContinue | Out-Null } $entries = [RawEnum]::Enum($Path) $dirs = @() $files = @() foreach ($e in $entries) { $full = Join-Path $Path $e.cFileName $attrs = $e.dwFileAttributes # Skip hidden and system items to match tree.com if ($attrs -band [IO.FileAttributes]::Hidden) { continue } if ($attrs -band [IO.FileAttributes]::System) { continue } $isDir = ($e.dwFileAttributes -band [IO.FileAttributes]::Directory) -ne 0 # Write-Host "FULL=[$full]" if ($isDir) { $dirs += [System.IO.DirectoryInfo]::new($full) } else { $files += [System.IO.FileInfo]::new($full) } } return [PSCustomObject]@{ Directories = $dirs Files = $files } } |