Private/Show-TreeInternal.ps1
|
# Show-Tree\Private\Show-TreeInternal.ps1 #region Entry Point <# .SYNOPSIS Core recursive engine for Show-Tree. .DESCRIPTION This function renders a directory tree using graphical connectors, optional color, optional file inclusion, and optional gap logic. It is called once from Show-Tree.ps1 and then recursively by itself. Responsibilities: • Normalize and validate the root path • Initialize gap state • Enumerate directories/files (raw Win32 or PowerShell) • Applies stable-order filtering using Get-FilteredTreeItems (Hidden/System/Include/Exclude with exact/glob precedence) • Render files, directories, and gap lines • Manage recursion depth and prefix construction • Maintain gap-mode state machine (Internal, Tail, Sibling) This function is internal-only and not exported. #> enum GapMode { None Internal Tail Sibling } function Show-TreeInternal { [CmdletBinding()] param ( # Absolute or relative path to render [string]$Path, # Tree.com compatibility mode [switch]$Tree, # Listing mode (indentation only) [switch]$List, # Maximum recursion depth (-1 = unlimited) [int]$MaxDepth, # Enable color output [switch]$Colorize, # Include files in output [switch]$IncludeFiles, # Hide hidden items [switch]$HideHidden, # Hide system items [switch]$HideSystem, # Show reparse point targets [switch]$ShowTargets, # Glob-based include/exclude filtering [string[]]$Exclude, [string[]]$Include, # Enable gap logic (blank lines between blocks) [switch]$Gap, # Use ASCII connectors instead of Unicode [switch]$Ascii, # Show attribute debug info [switch]$DebugAttributes, # Current recursion depth (internal) [int]$CurrentDepth = 0, # Prefix string built from parent connectors [string]$Prefix = "", # Whether the parent directory was the last sibling [bool]$IsLastParent = $false ) # Precompute ANSI sequences $esc = [char]27 $colorReset = $Colorize ? "${esc}[0m" : "" $colorGap = $Colorize ? "${esc}[90m" : "" # Precompute gap connector once per directory block $gapConnector = Get-Connector -Type Gap -Tree:$Tree -List:$List -Ascii:$Ascii # # Depth cap enforcement # if ($MaxDepth -ne -1 -and $CurrentDepth -ge $MaxDepth) { return } # # Directory enumeration # if ($Tree) { # Raw Win32 enumeration for Tree.com compatibility $raw = Get-RawDirectoryEntries -Path $Path $files = $IncludeFiles ? $raw.Files : @() $dirs = $raw.Directories } else { # Standard PowerShell enumeration $files = $IncludeFiles ? (Get-ChildItem -Path $Path -File -Force -ErrorAction SilentlyContinue) : @() $dirs = Get-ChildItem -Path $Path -Directory -Force -ErrorAction SilentlyContinue } # # Filtering # $dirs = Get‑FilteredTreeItems -Items $dirs -Include $Include -Exclude $Exclude -HideHidden:$HideHidden -HideSystem:$HideSystem $files = Get‑FilteredTreeItems -Items $files -Include $Include -Exclude $Exclude -HideHidden:$HideHidden -HideSystem:$HideSystem $fileCount = $files.Count $dirCount = $dirs.Count # Tree.com: suppress file connectors when no subdirectories exist $noSpan = $Tree -and $dirCount -eq 0 # # FILE RENDERING # for ($j = 0; $j -lt $fileCount; $j++) { $file = $files[$j] $isLastFile = ($j -eq $fileCount - 1) -and ($dirCount -eq 0) Write-TreeItem ` -Item $file ` -Type File ` -Prefix $Prefix ` -IsLast $isLastFile ` -Tree:$Tree ` -List:$List ` -Ascii:$Ascii ` -Colorize:$Colorize ` -ShowTargets:$ShowTargets ` -DebugAttributes:$DebugAttributes ` -Recurse:$false ` -NoSpan $noSpan ` -MaxDepth $MaxDepth ` -CurrentDepth $CurrentDepth ` -IncludeFiles:$IncludeFiles ` -Include $Include ` -Exclude $Exclude ` -Gap:$Gap ` -HideHidden:$HideHidden ` -HideSystem:$HideSystem } # # INTERNAL GAP (files → directories) # if ($Gap -and $script:GapState.LastGapMode -eq [GapMode]::None -and $IncludeFiles -and $fileCount -gt 0) { if ($dirCount -gt 0) { # Files + directories → connector gap Write-Gap $colorGap $Prefix $gapConnector $colorReset ([GapMode]::Internal) } else { # Files only → tail gap if ($Tree -or (-not $IsLastParent)) { Write-Gap $colorGap $Prefix $null $colorReset ([GapMode]::Tail) } } } # # DIRECTORY RENDERING # for ($i = 0; $i -lt $dirCount; $i++) { $dir = $dirs[$i] $isLastDir = ($i -eq $dirCount - 1) Write-TreeItem ` -Item $dir ` -Type Directory ` -Prefix $Prefix ` -IsLast $isLastDir ` -Tree:$Tree ` -List:$List ` -Ascii:$Ascii ` -Colorize:$Colorize ` -ShowTargets:$ShowTargets ` -DebugAttributes:$DebugAttributes ` -Recurse ` -NoSpan $false ` -MaxDepth $MaxDepth ` -CurrentDepth $CurrentDepth ` -IncludeFiles:$IncludeFiles ` -Include $Include ` -Exclude $Exclude ` -Gap:$Gap ` -HideHidden:$HideHidden ` -HideSystem:$HideSystem # # SIBLING / COUSIN GAP LOGIC # if ($Gap -and $i -lt $dirCount - 1) { # Tail gap suppresses immediate sibling gap if ($script:GapState.LastGapMode -eq [GapMode]::Tail) { $script:GapState.LastGapMode = [GapMode]::None continue } # Prevent consecutive gaps if ($script:GapState.LastGapMode -ne [GapMode]::None) { continue } # Normal mode: only if left sibling has visible children if (-not $Tree) { if (Test-HasChildrenForGap -Dir $dirs[$i] -CurrentDepth $CurrentDepth -MaxDepth $MaxDepth) { Write-Gap $colorGap $Prefix $gapConnector $colorReset ([GapMode]::Sibling) } } } } } #endregion #region Filtering <# .SYNOPSIS Returns a filtered subset of tree items using Hidden/System attributes and PowerShell‑style Include/Exclude glob patterns while preserving original order. .DESCRIPTION Get-FilteredTreeItems applies all Show-Tree filtering rules to a collection of filesystem items and returns the resulting subset in stable, original order. Filtering supports: • Hidden and System attribute removal (-HideHidden, -HideSystem) • PowerShell-style glob patterns for -Include and -Exclude • Exact-match and glob-match precedence rules • Include selectively overriding Exclude, Hidden, and System • Exclude exact-match patterns taking precedence over globbed Include patterns The function evaluates each item against four independent removal sets: Hidden, System, ExcludedExact, and ExcludedGlob. It also computes two inclusion sets: IncludedExact and IncludedGlob. Final item selection follows these rules: 1. Exact Include always wins. 2. Exact Exclude always wins, even if the item matches a broader Include glob. 3. Glob Include resurrects items removed by Hidden, System, or glob Exclude. 4. Hidden and System remove items unless resurrected by Include. 5. Glob Exclude removes items unless resurrected by Include. 6. Items not affected by any rule are kept. This produces intuitive, PowerShell-like filtering behavior while maintaining the original enumeration order required for correct tree rendering. .PARAMETER Items The collection of file or directory objects to filter. The function preserves the original ordering of this list. #> function Get‑FilteredTreeItems { param( [array]$Items, [string[]]$Include, [string[]]$Exclude, [switch]$HideHidden, [switch]$HideSystem ) if (-not $Items) { return @() } # # Capture original order # $orig = $Items # # Hidden/System sets # $hidden = $HideHidden ? ($orig | Where-Object { $_.Attributes -band [IO.FileAttributes]::Hidden }) : @() $system = $HideSystem ? ($orig | Where-Object { $_.Attributes -band [IO.FileAttributes]::System }) : @() # # Exclude sets (exact + glob) # $excludedExact = @() $excludedGlob = @() if ($Exclude) { foreach ($item in $orig) { $name = $item.Name if ($Exclude -contains $name) { $excludedExact += $item; continue } if ($Exclude | Where-Object { $name -like $_ }) { $excludedGlob += $item } } } # # Include sets (exact + glob) # $includedExact = @() $includedGlob = @() if ($Include) { foreach ($item in $orig) { $name = $item.Name if ($Include -contains $name) { $includedExact += $item; continue } if ($Include | Where-Object { $name -like $_ }) { $includedGlob += $item } } } # # Final filtering (stable order) # $final = foreach ($item in $orig) { $name = $item.Name $isHidden = $hidden -contains $item $isSystem = $system -contains $item $isExcludedExact = $excludedExact -contains $item $isExcludedGlob = $excludedGlob -contains $item $isIncludedExact = $includedExact -contains $item $isIncludedGlob = $includedGlob -contains $item # # Decision logic # if ($isIncludedExact) { $item; continue } # exact include wins if ($isExcludedExact) { continue } # exact exclude wins if ($isIncludedGlob) { $item; continue } # glob include resurrects if ($isHidden) { continue } # hidden removes unless included if ($isSystem) { continue } # system removes unless included if ($isExcludedGlob) { continue } # glob exclude removes unless included $item } return $final } #endregion #region Rendering (TreeItem, Gap) <# .SYNOPSIS Renders a single file or directory entry. .DESCRIPTION Handles: • Connector selection • Style/color application • Reparse target display • Attribute debug output • Recursion into subdirectories • Gap-state reset #> function Write-TreeItem { param( [Parameter(Mandatory)] $Item, [Parameter(Mandatory)] [ValidateSet('File','Directory')] [string]$Type, # Prefix inherited from parent [string]$Prefix = "", # Whether this item is the last sibling [bool]$IsLast, # Mode switches [switch]$Tree, [switch]$List, [switch]$Ascii, [switch]$Colorize, [switch]$ShowTargets, [switch]$DebugAttributes, [switch]$Recurse, # Whether to suppress file connector span [bool]$NoSpan = $false, # Recursion state [int]$MaxDepth, [int]$CurrentDepth, # Additional flags [switch]$IncludeFiles, [string[]]$Exclude, [string[]]$Include, [switch]$Gap, [switch]$HideHidden, [switch]$HideSystem ) # Compute connector for this item $connector = Get-Connector ` -Type $Type ` -Tree:$Tree ` -List:$List ` -Ascii:$Ascii ` -IsLast $IsLast ` -NoSpan $NoSpan # Compute style $style = Get-ItemStyle -Item $Item -Colorize:$Colorize # # Reparse target resolution # $target = $null if ($ShowTargets -and ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { $info = Get-Item -LiteralPath $Item.FullName -Force -ErrorAction SilentlyContinue if ($info -and $info.PSObject.Properties.Match('Target')) { $target = $info.Target } } # # Output formatting # $esc = [char]27 $reset = $Colorize ? "${esc}[0m" : "" $dim = $Colorize ? "${esc}[90m" : "" $targetText = $target ? " ${dim}->${reset} $target" : "" # Optional attribute debug $debug = "" if ($DebugAttributes) { $styleName = $style.Name $attrHex = ('0x{0:X8}' -f [uint32]$Item.Attributes) $attrNames = $Item.Attributes.ToString() $debug = " [$attrHex $attrNames | $styleName]" } Write-Output "${dim}${Prefix}${dim}${connector}$($style.Ansi)$($Item.Name)$reset$targetText$debug" # # Reset gap state unless tail gap was printed # if ($script:GapState.LastGapMode -ne [GapMode]::Tail) { $script:GapState.LastGapMode = [GapMode]::None } # # Recursion into subdirectories # if ($Recurse -and $Type -eq 'Directory' -and -not ($Item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { # Build next-level prefix $newPrefix = $Prefix + (Get-Connector ` -Type Prefix ` -Tree:$Tree ` -List:$List ` -Ascii:$Ascii ` -IsLast $IsLast) # Recurse Show-TreeInternal ` -Path $Item.FullName ` -Tree:$Tree ` -List:$List ` -MaxDepth $MaxDepth ` -Colorize:$Colorize ` -IncludeFiles:$IncludeFiles ` -HideHidden:$HideHidden ` -HideSystem:$HideSystem ` -ShowTargets:$ShowTargets ` -Exclude $Exclude ` -Include $Include ` -Gap:$Gap ` -Ascii:$Ascii ` -DebugAttributes:$DebugAttributes ` -CurrentDepth ($CurrentDepth + 1) ` -Prefix $newPrefix ` -IsLastParent $IsLast } # # Reset gap state again after recursion # if ($script:GapState.LastGapMode -ne [GapMode]::Tail) { $script:GapState.LastGapMode = [GapMode]::None } } <# .SYNOPSIS Writes a gap line between blocks. .DESCRIPTION Handles Internal, Tail, and Sibling gap modes. Updates the global gap-state machine. #> function Write-Gap { param( $colorGap, $Prefix, $GapConnector, $colorReset, [GapMode]$Mode ) $connector = $GapConnector ? $GapConnector : "" Write-Output "${colorGap}${Prefix}${connector}${colorReset}" $script:GapState.LastGapMode = $Mode } #endregion #region Gap Logic Helpers <# .SYNOPSIS Determines whether a directory has visible children. .DESCRIPTION Used to decide whether to print a sibling/cousin gap. Respects MaxDepth and treats reparse points as leaf nodes. #> function Test-HasChildrenForGap { param( $Dir, [int]$CurrentDepth, [int]$MaxDepth ) if (Test-IsReparsePoint $Dir) { return $false } # Depth cap: treat as empty if recursion would stop here if ($MaxDepth -ne -1 -and $CurrentDepth + 1 -ge $MaxDepth) { return $false } $children = Get-ChildItem -LiteralPath $Dir.FullName -Force -ErrorAction SilentlyContinue return $children.Count -gt 0 } <# .SYNOPSIS Checks whether an item is a reparse point. .DESCRIPTION Reparse points (symlinks/junctions) are treated as leaf nodes for recursion and gap logic. #> function Test-IsReparsePoint { param($Item) [bool]($Item.Attributes -band [IO.FileAttributes]::ReparsePoint) } #endregion #region Connector Rendering <# .SYNOPSIS Returns the connector string for a given item type. .DESCRIPTION Handles: • Tree.com ASCII mode • Unicode graphical mode • Prefix vs File vs Directory vs Gap • Last-sibling logic • NoSpan suppression for Tree.com file connectors #> function Get-Connector { param( [Parameter(Mandatory)] [ValidateSet('File','Directory','Gap','Prefix')] [string]$Type, [switch]$Tree, [switch]$List, [switch]$Ascii, [bool]$IsLast = $false, [bool]$NoSpan = $false ) # # Listing mode: indentation only # if ($List) { return ' ' } # # Tree.com compatibility mode # if ($Tree) { if ($Type -eq 'File' -and $NoSpan) { return ' ' } switch ($Type) { 'File' { return $Ascii ? '| ' : '│ ' } 'Directory' { if ($IsLast) { return $Ascii ? '\---' : '└───' } else { return $Ascii ? '+---' : '├───' } } 'Gap' { return $Ascii ? '|' : '│' } 'Prefix' { if ($IsLast) { return ' ' } else { return $Ascii ? '| ' : '│ ' } } } } # # Graphical Unicode mode (Show‑Tree default) # switch ($Type) { 'File' { if ($IsLast) { return $Ascii ? '\-- ' : '╙── ' } else { return $Ascii ? '+-- ' : '╟── ' } } 'Directory' { if ($IsLast) { return $Ascii ? '\== ' : '╚══ ' } else { return $Ascii ? '+== ' : '╠══ ' } } 'Gap' { return $Ascii ? '|' : '║' } 'Prefix' { if ($IsLast) { return ' ' } else { return $Ascii ? '| ' : '║ ' } } } } #endregion #region Style Rendering <# .SYNOPSIS Computes the ANSI style for a file or directory. .DESCRIPTION Applies: • Base style (directory/file/symlink/junction) • Attribute overlays (hidden, system, temporary, etc.) • Foreground overrides • Combined ANSI escape sequence Returns an object: @{ Name = "..."; Ansi = "..."; } #> function Get-ItemStyle { param( $Item, $Colorize, $StyleProfile = $script:StyleProfile ) $esc = [char]27 $isDir = $Item.PSIsContainer $attrs = $Item.Attributes $isReparse = [bool]($attrs -band [IO.FileAttributes]::ReparsePoint) # # Determine base style # if ($isReparse -and $isDir) { $styleName = "Junction" $base = $StyleProfile.Base.Junction } elseif ($isReparse -and -not $isDir) { $styleName = "Symlink" $base = $StyleProfile.Base.File } elseif ($isDir) { $styleName = "Directory" $base = $StyleProfile.Base.Directory } else { $styleName = "File" $base = $StyleProfile.Base.File } # # No color mode # if (-not $Colorize) { return [PSCustomObject]@{ Name = $styleName Ansi = "" } } # # Parse base style codes # $codes = @() + ($base -split ';' | Where-Object { $_ -ne '' }) # Extract foreground codes (30–37, 90–97) $fg = @() + ($codes | Where-Object { $_ -match '^(3[0-7]|9[0-7])$' }) $codes = @() + ($codes | Where-Object { $_ -notmatch '^(3[0-7]|9[0-7])$' }) # # Apply attribute overlays # foreach ($flag in Get-SetFileAttributes $attrs) { $flagName = $flag.ToString() if ($StyleProfile.Attributes.ContainsKey($flagName)) { $overlay = $StyleProfile.Attributes[$flagName] # Add overlay attributes if ($overlay.Attributes) { $codes += ($overlay.Attributes -split ';') } # Foreground override if ($overlay.OverrideForeground) { if ($overlay.OverrideForeground -is [string]) { $fg = $overlay.OverrideForeground } elseif ($overlay.OverrideForeground.ContainsKey($styleName)) { $fg = $overlay.OverrideForeground[$styleName] } } } } # # Build final ANSI sequence # $final = @() if ($fg) { $final += $fg } $final += $codes $ansi = "${esc}[$($final -join ';')m" [PSCustomObject]@{ Name = $styleName Ansi = $ansi } } #endregion #region Path Utilities <# .SYNOPSIS Enumerates all set file attributes on an item. .DESCRIPTION Used by Get-ItemStyle to apply attribute overlays. #> function Get-SetFileAttributes { param([IO.FileAttributes]$Attributes) foreach ($flag in [System.Enum]::GetValues([IO.FileAttributes])) { if ($Attributes -band $flag) { $flag } } } <# .SYNOPSIS Normalizes a path to match actual filesystem casing. .DESCRIPTION Walks each segment and resolves its real casing using Get-ChildItem. Ensures consistent display even when user input is lowercase/mixed. #> function Get-NormalizedPath { param([string]$Path = ".") $absPath = [System.IO.Path]::GetFullPath($Path) # Trim trailing slash unless 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 -ErrorAction Stop | 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 { # Parent doesn't exist — keep original casing $normalized += $segment $current = Join-Path $current $segment -ErrorAction Stop } } ($normalized -join '\') } <# .SYNOPSIS Finds the nearest existing parent directory. .DESCRIPTION Used for Tree.com header generation when the target path does not fully exist. #> 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 } $current = $parent.FullName } $current } <# .SYNOPSIS Returns the filesystem label for a drive. .DESCRIPTION Used only in Tree.com compatibility mode. #> function Get-VolumeName { param([string]$Path = ".") $driveLetter = (Get-Item $Path).PSDrive.Name $volume = Get-Volume -DriveLetter $driveLetter $volume.FileSystemLabel } <# .SYNOPSIS Retrieves the volume serial number using Win32 API. .DESCRIPTION Matches Tree.com output exactly. #> function Get-VolumeSerialNumber { param ( [string]$Path = "." ) 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) "{0:X4}-{1:X4}" -f $serialHigh, $serialLow } #endregion #region Raw Directory Enumeration <# .SYNOPSIS Enumerates directory entries using Win32 FindFirstFile. .DESCRIPTION Used in Tree.com mode to match exact ordering and behavior. Returns PSCustomObject with: • Directories = [...] • Files = [...] #> function Get-RawDirectoryEntries { param([string]$Path) # # Load RawEnum type once # 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 } # # Enumerate entries # $entries = [RawEnum]::Enum($Path) $dirs = @() $files = @() foreach ($e in $entries) { $isDir = ($e.dwFileAttributes -band [IO.FileAttributes]::Directory) -ne 0 $root = Get-Item -LiteralPath (Join-Path $Path $e.cFileName) -Force -ErrorAction SilentlyContinue if (-not $root) { continue } $item = [PSCustomObject]@{ FullName = $root.FullName Name = $root.Name Attributes = $root.Attributes PSIsContainer = $isDir } if ($isDir) { $item.PSObject.TypeNames.Insert(0, 'System.IO.DirectoryInfo') $dirs += $item } else { $item.PSObject.TypeNames.Insert(0, 'System.IO.FileInfo') $files += $item } } # # Return structured result # [PSCustomObject]@{ Directories = $dirs Files = $files } } #endregion |