Public/Output/Show-Tree.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."

      #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
    }
  }

  Begin {
    if (!$Path -and $psCmdlet.ParameterSetName -eq "Path") {
      $Path = Get-Location
    }

    if ($PSBoundParameters.containskey("InColor")) {
      $Colorize = $True
      $script:top = ($script:PSAnsiFileMap).where( { $_.description -eq 'TopContainer' }).Ansi
      $script:child = ($script:PSAnsiFileMap).where( { $_.description -eq 'ChildContainer' }).Ansi
    }
    function GetIndentString {
      [CmdletBinding()]
      Param([bool[]]$IsLast)

      # $numPadChars = 1
      $str = ''
      for ($i = 0; $i -lt $IsLast.Count - 1; $i++) {
        $sepChar = if ($IsLast[$i]) { ' ' } else { '┃' }
        $str += "$sepChar"
        $str += " " * ($IndentSize - 1)
      }
      $teeChar = if ($IsLast[-1]) { '╰' } else { '┃' }
      $str += "$teeChar"
      $str += "━" * ($IndentSize - 1)
      $str
    }
    function ShowProperty() {
      [cmdletbinding()]
      Param(
        [string]$Name,
        [string]$Value,
        [bool[]]$IsLast
      )
      $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
    }
    function ShowItem {
      [CmdletBinding()]
      Param(
        [string]$Path,
        [string]$Name,
        [bool[]]$IsLast,
        [bool]$HasChildItems = $false,
        [switch]$Color,
        [ValidateSet("topcontainer", "childcontainer", "file")]
        [string]$ItemType
      )
      if ($IsLast.Count -eq 0) {
        if ($Color) {
          # Write-Output "$([char]0x1b)[38;2;0;255;255m$("$(Resolve-Path $Path)")$([char]0x1b)[0m"
          Write-Output "$($script:top)$("$(Resolve-Path $Path)")$([char]0x1b)[0m"
        } else {
          "$(Resolve-Path $Path)"
        }
      } else {
        $indentStr = GetIndentString $IsLast
        if ($Color) {
          #ToDo - define a user configurable color map
          Switch ($ItemType) {
            "topcontainer" {
              Write-Output "$indentStr$($script:top)$($Name)$([char]0x1b)[0m"
              #Write-Output "$indentStr$([char]0x1b)[38;2;0;255;255m$("$Name")$([char]0x1b)[0m"
            }
            "childcontainer" {
              Write-Output "$indentStr$($script:child)$($Name)$([char]0x1b)[0m"
              #Write-Output "$indentStr$([char]0x1b)[38;2;255;255;0m$("$Name")$([char]0x1b)[0m"
            }
            "file" {
              #only use map items with regex patterns
              foreach ($item in ($script:PSAnsiFileMap | Where-Object Pattern)) {
                if ($name -match $item.pattern -AND (!$done)) {
                  Write-Output "$indentStr$($item.ansi)$($Name)$([char]0x1b)[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 (!$done) {
                # No ansi match for $Name
                Write-Output "$indentStr$Name$([char]0x1b)[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 (!$HasChildItems)
          $showParams = @{
            Name   = $prop.Name
            Value  = $prop.Value
            IsLast = $IsLast
          }
          ShowProperty @showParams
        }
      }
    }
    function ShowContainer {
      [CmdletBinding()]
      Param (
        [string]$Path,
        [string]$Name = $(Split-Path $Path -Leaf),
        [bool[]]$IsLast = @(),
        [switch]$IsTop,
        [switch]$Color
      )
      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
        }
      }
    }
  }

  Process {
    if ($psCmdlet.ParameterSetName -eq "Path") {
      # In the -Path (non-literal) resolve path in case it is wildcarded.
      $resolvedPaths = @($Path | Resolve-Path | ForEach-Object { $_.Path })
    } else {
      # Must be -LiteralPath
      $resolvedPaths = @($LiteralPath)
    }
    foreach ($rpath in $resolvedPaths) {
      $showParams = @{
        Path  = $rpath
        Color = $colorize
        IsTop = $True
      }
      ShowContainer @showParams
    }
  }
}