functions/ShowTree.ps1
function Show-Tree { [CmdletBinding(DefaultParameterSetName = 'Path')] [alias('pstree', 'shtree')] Param( [Parameter( Position = 0, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [alias('FullName')] [string[]]$Path = '.', [Parameter( Position = 0, ParameterSetName = 'LiteralPath', ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string[]]$LiteralPath, [Parameter(Position = 1)] [ValidateRange(0, 2147483647)] [int]$Depth = [int]::MaxValue, [Parameter()] [ValidateRange(1, 100)] [int]$IndentSize = 3, [Parameter()] [alias('files')] [switch]$ShowItem, [Parameter(HelpMessage = 'Display item properties. Use * to show all properties or specify a comma separated list.')] [alias('properties')] [string[]]$ShowProperty ) DynamicParam { #define the InColor parameter if the path is a FileSystem path if ($PSBoundParameters.ContainsKey('Path')) { $here = $PSBoundParameters['Path'] } elseif ($PSBoundParameters.ContainsKey('LiteralPath')) { $here = $PSBoundParameters['LiteralPath'] } else { $here = (Get-Location).path } if (((Get-Item -Path $here).PSprovider.Name -eq 'FileSystem' ) -OR ((Get-Item -LiteralPath $here).PSprovider.Name -eq 'FileSystem')) { #define a parameter attribute object $attributes = New-Object System.Management.Automation.ParameterAttribute $attributes.HelpMessage = 'Show tree and item colorized for the filesystem.' #add an alias $alias = [System.Management.Automation.AliasAttribute]::new('ansi') #define a collection for attributes $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $attributeCollection.Add($attributes) $attributeCollection.Add($alias) #define the dynamic param $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('InColor', [Switch], $attributeCollection) #create array of dynamic parameters $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary $paramDictionary.Add('InColor', $dynParam1) #use the array return $paramDictionary } } #DynamicParam Begin { Write-Verbose "Starting $($MyInvocation.MyCommand)" if (-Not $Path -and $PSCmdlet.ParameterSetName -eq 'Path') { $Path = Get-Location } if ($PSBoundParameters.ContainsKey('InColor')) { $Colorize = $True #30 May 2024 Use PSStyle.FileInfo if found if ($PSStyle.FileInfo) { $script:top = $PSstyle.FileInfo.Directory $script:child = $PSstyle.FileInfo.Directory } else { $script:top = ($global:PSAnsiFileMap).where( { $_.description -eq 'TopContainer' }).Ansi $script:child = ($global:PSAnsiFileMap).where( { $_.description -eq 'ChildContainer' }).Ansi } } function GetIndentString { [CmdletBinding()] Param([bool[]]$IsLast) Write-Verbose "Starting $($MyInvocation.MyCommand)" # $numPadChars = 1 $str = '' for ($i = 0; $i -lt $IsLast.Count - 1; $i++) { $sepChar = if ($IsLast[$i]) { ' ' } else { '|' } $str += "$sepChar" $str += ' ' * ($IndentSize - 1) } #The \ indicates the item is the last in the container $teeChar = if ($IsLast[-1]) { '\' } else { '+' } $str += "$teeChar" $str += '-' * ($IndentSize - 1) $str Write-Verbose "Ending $($MyInvocation.MyCommand)" } function ShowProperty() { [cmdletbinding()] Param( [string]$Name, [string[]]$Value, [bool[]]$IsLast ) Write-Verbose "Starting $($MyInvocation.MyCommand)" $indentStr = GetIndentString $IsLast $propStr = "${indentStr} $Name = " $availableWidth = $host.UI.RawUI.BufferSize.Width - $propStr.Length - 1 if ($Value.Length -gt $availableWidth) { $ellipsis = '...' $val = $Value.Substring(0, $availableWidth - $ellipsis.Length) + $ellipsis } else { $val = $Value } $propStr += $val $propStr Write-Verbose "Ending $($MyInvocation.MyCommand)" } function ShowItem { [CmdletBinding()] Param( [string]$Path, [string]$Name, [bool[]]$IsLast, [bool]$HasChildItems = $false, [switch]$Color, [ValidateSet('TopContainer', 'ChildContainer', 'file')] [string]$ItemType ) Write-Verbose "Starting $($MyInvocation.MyCommand)" $PSBoundParameters | Out-String | Write-Verbose if ($IsLast.Count -eq 0) { if ($Color) { # Write-Output "$([char]27)[38;2;0;255;255m$("$(Resolve-Path $Path)")$([char]27)[0m" Write-Output "$($script:top)$("$(Resolve-Path $Path)")$([char]27)[0m" } else { "$(Resolve-Path $Path)" } } else { $indentStr = GetIndentString $IsLast if ($Color) { Switch ($ItemType) { 'TopContainer' { Write-Output "$indentStr$($script:top)$($Name)$([char]27)[0m" #Write-Output "$indentStr$([char]27)[38;2;0;255;255m$("$Name")$([char]27)[0m" } 'ChildContainer' { Write-Output "$indentStr$($script:child)$($Name)$([char]27)[0m" #Write-Output "$indentStr$([char]27)[38;2;255;255;0m$("$Name")$([char]27)[0m" } 'file' { #30 May 2024 Use PSStyle.FileInfo if found if ($PSStyle.FileInfo) { if ($name -match "\.exe$") { Write-Output "$indentStr$($PSStyle.FileInfo.Executable)$($Name)$([char]27)[0m" } else { $ext = $name.Split('.')[-1] Write-Output "$indentStr$($PSStyle.FileInfo.Extension[".$ext"])$($Name)$([char]27)[0m" } $done = $True } else { #only use map items with regex patterns foreach ($item in ($global:PSAnsiFileMap | Where-Object Pattern)) { if ($name -match $item.pattern -AND (-not $done)) { Write-Verbose "Detected a $($item.description) file" Write-Output "$indentStr$($item.ansi)$($Name)$([char]27)[0m" #set a flag indicating we've made a match to stop looking $done = $True } } } #no match was found so just write the item. if (-Not $done) { Write-Verbose "No ansi match for $Name" Write-Output "$indentStr$Name$([char]27)[0m" } } #file Default { Write-Output "$indentStr$Name" } } #switch } #if color else { "$indentStr$Name" } } if ($ShowProperty) { $IsLast += @($false) $excludedProviderNoteProps = 'PSChildName', 'PSDrive', 'PSParentPath', 'PSPath', 'PSProvider' $props = @(Get-ItemProperty $Path -ea 0) if ($props[0] -is [PSCustomObject]) { if ($ShowProperty -eq '*') { $props = @($props[0].PSObject.properties | Where-Object { $excludedProviderNoteProps -NotContains $_.Name }) } else { $props = @($props[0].PSObject.properties | Where-Object { $excludedProviderNoteProps -NotContains $_.Name -AND $ShowProperty -contains $_.name }) } } for ($i = 0; $i -lt $props.Count; $i++) { $prop = $props[$i] $IsLast[-1] = ($i -eq $props.count - 1) -and (-Not $HasChildItems) #30 May 2024 better accommodate binary values in the registry if ($prop.Value -is [byte[]]) { $Value = 'Binary or byte array' } else { $Value = $prop.Value } $showParams = @{ Name = $prop.Name Value = $Value IsLast = $IsLast } ShowProperty @showParams } } Write-Verbose "Ending $($MyInvocation.MyCommand)" } function ShowContainer { [CmdletBinding()] Param ( [string]$Path, [string]$Name = $(Split-Path $Path -Leaf), [bool[]]$IsLast = @(), [switch]$IsTop, [switch]$Color ) Write-Verbose "Starting $($MyInvocation.MyCommand) on $Path" $PSBoundParameters | Out-String | Write-Verbose if ($IsLast.Count -gt $Depth) { return } $childItems = @() if ($IsLast.Count -lt $Depth) { try { $rPath = Resolve-Path -LiteralPath $Path -ErrorAction stop } catch { Throw "Failed to resolve $path. This PSProvider and path may be incompatible with this command." #bail out return } $childItems = @(Get-ChildItem $rPath -ErrorAction $ErrorActionPreference | Where-Object { $ShowItem -or $_.PSIsContainer }) } $hasChildItems = $childItems.Count -gt 0 # Show the current container $sParams = @{ path = $Path name = $Name IsLast = $IsLast hasChildItems = $hasChildItems Color = $Color ItemType = If ($isTop) { 'TopContainer' } else { 'ChildContainer' } } ShowItem @sParams # Process the children of this container $IsLast += @($false) for ($i = 0; $i -lt $childItems.count; $i++) { $childItem = $childItems[$i] $IsLast[-1] = ($i -eq $childItems.count - 1) if ($childItem.PSIsContainer) { $iParams = @{ path = $childItem.PSPath name = $childItem.PSChildName isLast = $IsLast Color = $color } ShowContainer @iParams } elseif ($ShowItem) { $unresolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($childItem.PSPath) $name = Split-Path $unresolvedPath -Leaf $iParams = @{ Path = $childItem.PSPath Name = $name IsLast = $IsLast Color = $Color ItemType = 'File' } ShowItem @iParams } } Write-Verbose "Ending $($MyInvocation.MyCommand)" } } #begin Process { Write-Verbose "Detected parameter set $($PSCmdlet.ParameterSetName)" if ($PSCmdlet.ParameterSetName -eq 'Path') { # In the -Path (non-literal) resolve path in case if it is wildcarded. $resolvedPaths = @($Path | Resolve-Path | ForEach-Object { $_.Path }) } else { # Must be -LiteralPath $resolvedPaths = @($LiteralPath) } Write-Verbose 'Using these PSBoundParameters' $PSBoundParameters | Out-String | Write-Verbose foreach ($rPath in $resolvedPaths) { Write-Verbose "Processing $rPath" $showParams = @{ Path = $rPath Color = $colorize IsTop = $True } ShowContainer @showParams } } #process end { Write-Verbose "Ending $($MyInvocation.MyCommand)" } } |