9
\$\begingroup\$

For anyone reading this old question, this module has evolved and improved a lot since then. It has been fully rewritten in C# and uploaded to the PowerShell Gallery! If you would like to try it out:

Install-Module PSTree -Scope CurrentUser

And, if you want to check out the source code: PSTree GitHub. This project is always open to collaborations and all feedback is appreciated!


This Module started as a fun coding exercise designed to emulate the tree command and also calculate the size of each folder based of the files size of that folder (non-recursive).

Now, completed and tested on Windows and Linux, I thought it would be nice to have it reviewed to see where it could be improved improved, or if there is something I could have missed and should fix.

If I'm failing to explain anything or make things clear, please let me know and I'll try to correct it. I would like to thank you upfront for taking the time to read this and I'm sorry for the length of the post.

Helper Functions

Indent

Takes a string as input and based on -Indent argument, it returns the indented string. Example:

PS /> Indent ThisString -Indent 4

                ThisString
  • Code
function Indent {
param(
    [String]$String,
    [Int]$Indent
)

    switch($Indent)
    {
        {-not $_} {
            return $String
        }
        Default {
            "$('    ' * $_)$String"
        }
    }
}

SizeConvert

This one is pretty straight forward, all credits to this answer, I just modified it a bit:

PS /> SizeConvert 1024                   

1 Kb
PS /> SizeConvert (1024*1024)

1 Mb
  • Code
function SizeConvert($Size) {
    $suffix = "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"
    $index = 0
    while ($Size -ge 1kb) 
    {
        $Size /= 1kb
        $index++
    }
    [string]::Format(
        '{0:0.##} {1}',
        $Size, $suffix[$index]
    )
}

Get-FolderRecursive

This function is the heart, takes a folder's path as input and will recurse through the sub-folders, the output received by it is what we see on the $outObject scriptblock.

EXAMPLE

PS /> Get-FolderRecursive /etc/ -Depth 2 -EA SilentlyContinue | Select-Object -First 5 | Format-Table

Nesting Hierarchy        Size      RawSize FullName          Parent    Creat....
------- ---------        ----      ------- --------          ------    -----....
      0 etc              349.83 KB  358229 /etc/             /         12/14....
      1     acpi         1.83 KB      1872 /etc/acpi         /etc      8/14/....
      2         events   1.44 KB      1470 /etc/acpi/events  /etc/acpi 8/14/....
      1     alternatives 5.08 KB      5201 /etc/alternatives /etc      3/28/....
      1     apache2      0 B               /etc/apache2      /etc      8/14/....
  • Code
function Get-FolderRecursive {
[cmdletbinding()]
param(
    [string]$Path,
    [int]$Nesting = 0,
    [int]$Depth,
    [switch]$Deep,
    [switch]$Force
)

    $outObject = {
        param($Nesting, $folder)
        
        $files = Get-ChildItem -LiteralPath $folder.FullName -File -Force
        $size = ($files | Measure-Object Length -Sum).Sum
        
        [pscustomobject]@{
            Nesting        = $Nesting
            Hierarchy      = Indent -String $folder.Name -Indent $Nesting
            Size           = SizeConvert $size
            RawSize        = $size
            FullName       = $folder.FullName
            Parent         = $folder.Parent
            CreationTime   = $folder.CreationTime
            LastAccessTime = $folder.LastAccessTime
            LastWriteTime  = $folder.LastWriteTime
        }
    }

    if(-not $Nesting)
    {
        $parent = Get-Item -LiteralPath $Path
        & $outObject -Nesting $Nesting -Folder $parent
    }

    $Nesting++

    $folders = if($Force.IsPresent)
    {
        Get-ChildItem -LiteralPath $Path -Directory -Force
    }
    else
    {
        Get-ChildItem -LiteralPath $Path -Directory
    }

    foreach($folder in $folders)
    {
        & $outObject -Nesting $Nesting -Folder $folder
        
        $PSBoundParameters.Path = $folder.FullName
        $PSBoundParameters.Nesting = $Nesting

        if($PSBoundParameters.ContainsKey('Depth'))
        {
            if($Nesting -lt $Depth)
            {
                Get-FolderRecursive @PSBoundParameters
            }
        }
        
        if($Deep.IsPresent)
        {
            Get-FolderRecursive @PSBoundParameters
        }
    }
}

DrawHierarchy

This function takes as input an array and will draw the hierarchy lines based on the arguments provided to -PropertyName and -RecursionProperty. This one is pretty hard for me to explain, and my only concern is understanding if the regex └|\S ($corner is on the code) will suffice and in case it is not how could it be improved. Example using the result from before:

PS /> $result = Get-FolderRecursive /etc/ -Depth 2 -EA SilentlyContinue |Select-Object -First 5
PS /> DrawHierarchy -Array $result -PropertyName Hierarchy -RecursionProperty Nesting | Format-Table

Nesting Hierarchy        Size      RawSize FullName          Parent    Creat.....
------- ---------        ----      ------- --------          ------    -----.....
      0 etc              349.83 KB  358229 /etc/             /         12/14.....
      1 ├── acpi         1.83 KB      1872 /etc/acpi         /etc      8/14/.....
      2 │   └── events   1.44 KB      1470 /etc/acpi/events  /etc/acpi 8/14/.....
      1 ├── alternatives 5.08 KB      5201 /etc/alternatives /etc      3/28/.....
      1 └── apache2      0 B               /etc/apache2      /etc      8/14/.....
  • Code
function DrawHierarchy {
param(
    [System.Collections.ArrayList]$Array,
    [string]$PropertyName,
    [string]$RecursionProperty
)
    # Had to do this because of Windows PowerShell Default Encoding
    # Not good at enconding stuff, probably a better way. Sorry for the ugliness :(
    $bytes = @(
        '226','148','148'
        '44','226','148'
        '128','44','226'
        '148','130','44'
        '226','148','156'
    )
    $corner, $horizontal, $pipe, $connector =
    [System.Text.Encoding]::UTF8.GetString($bytes).Split(',')
    $cornerConnector = "${corner}$(${horizontal}*2) "

    $Array | Group-Object $RecursionProperty |
    Select-Object -Skip 1 | ForEach-Object {
        $_.Group | ForEach-Object {
            $_.$PropertyName = $_.$PropertyName -replace '\s{4}(?=\S)', $cornerConnector
        }
    }

    for($i = 1; $i -lt $Array.Count; $i++)
    {
        $index = $Array[$i].$PropertyName.IndexOf($corner)
        if($index -ge 0)
        {
            $z = $i - 1
            while($Array[$z].$PropertyName[$index] -notmatch "$corner|\S")
            {
                $replace = $Array[$z].$PropertyName.ToCharArray()
                $replace[$Index] = $pipe
                $Array[$z].$PropertyName = -join $replace
                $z--
            }
        
            if($Array[$z].$PropertyName[$index] -eq $corner)
            {
                $replace = $Array[$z].$PropertyName.ToCharArray()
                $replace[$Index] = $connector
                $Array[$z].$PropertyName = -join $replace
            }
        }
    }
    $Array
}

Wrapper Function

Get-PSTree

Lastly, the wrapper, this function is in charge of parameter validation mainly and calling both Get-FolderRecursive and DrawHierarchy based on the arguments provided to -Path and -Depth or -Deep with -Force being optional.

  • PARAMETER
Parameter Name Description
-Path <string> Absolute or relative folder path. Alias: FullName, PSPath
[-Depth <int>] Specifies the maximum level of recursion
[-Deep <switch>] Recursion until maximum level is reached
[-Force <switch>] Display hidden and system folders
[<CommonParameters>] See about_CommonParameters
  • OUTPUTS Object[]
Name           TypeNameOfValue
----           ---------------
Nesting        System.Int32
Hierarchy      System.String
Size           System.String
RawSize        System.Double
FullName       System.String
Parent         System.IO.DirectoryInfo
CreationTime   System.DateTime
LastAccessTime System.DateTime
LastWriteTime  System.DateTime
  • Code
function Get-PSTree {
[cmdletbinding(DefaultParameterSetName = 'Depth')]
[alias('gpstree')]
param(
    [parameter(
        Mandatory,
        ParameterSetName = 'Path',
        Position = 0
    )]
    [parameter(
        Mandatory,
        ParameterSetName = 'Depth',
        Position = 0
    )]
    [parameter(
        Mandatory,
        ParameterSetName = 'Max',
        Position = 0
    )]
    [alias('FullName', 'PSPath')]
    [ValidateScript({ 
        if(Test-Path $_ -PathType Container)
        {
            return $true
        }
        throw 'Invalid Folder Path'
    })]
    [string]$Path,
    [ValidateRange(1, [int]::MaxValue)]
    [parameter(
        ParameterSetName = 'Depth',
        Position = 1
    )]
    [int]$Depth = 3,
    [parameter(
        ParameterSetName = 'Max',
        Position = 1
    )]
    [switch]$Deep,
    [switch]$Force
)

    process
    {
        [Environment]::CurrentDirectory = $pwd.ProviderPath
        $PSBoundParameters.Path = ([System.IO.FileInfo]$Path).FullName
        $DefaultProps = @(
            'Hierarchy'
            'Size'
        )
        [Management.Automation.PSMemberInfo[]]$standardMembers =
            [System.Management.Automation.PSPropertySet]::new(
                'DefaultDisplayPropertySet',
                [string[]]$DefaultProps
            )
        
        $hash = @{
            Name = 'PSStandardMembers'
            MemberType = 'MemberSet'
            Value = $standardMembers
        }
        
        $isDepthParam = $PSCmdlet.ParameterSetName -eq 'Depth'
        $containsDepth = $PSBoundParameters.ContainsKey('Depth')
        
        if($isDepthParam -and -not $containsDepth)
        {
            $PSBoundParameters.Add('Depth', $Depth)
        }

        $result = Get-FolderRecursive @PSBoundParameters

        $drawProps = @{
            Array = $result
            PropertyName = 'Hierarchy'
            RecursionProperty = 'Nesting'
        }

        DrawHierarchy @drawProps | ForEach-Object {
            $hash.InputObject = $_
            Add-Member @hash
        }

        $result
    }
}

EXAMPLES

  • Get-PSTree . Gets the hierarchy and folder size of the current directory using default Depth (3).
  • Get-PSTree C:\users\user -Depth 10 -Force Gets the hierarchy and folder size, including hidden ones, of the user directory with a maximum of 10 levels of recursion.
  • Get-PSTree /home/user -Deep: Gets the hierarchy and folder size of the user directory and all folders below.

PS /etc> $hierarchy = gpstree . -ErrorAction SilentlyContinue -Depth 5
PS /etc> $hierarchy | Select -First 20                                

Hierarchy                              Size
---------                              ----
etc                                    349.3 Kb
├── acpi                               1.8 Kb
│   └── events                         1.4 Kb
├── alternatives                       5.1 Kb
├── apache2                            0 Bytes
│   ├── conf-available                 127 Bytes
│   └── mods-available                 156 Bytes
├── apm                                0 Bytes
│   ├── event.d                        2.9 Kb
│   ├── resume.d                       17 Bytes
│   ├── scripts.d                      228 Bytes
│   └── suspend.d                      17 Bytes
├── apparmor                           3.4 Kb
│   └── init                           0 Bytes
│       └── network-interface-security 33 Bytes
├── apparmor.d                         25.9 Kb
│   ├── abstractions                   91.1 Kb
│   │   ├── apparmor_api               2.4 Kb
│   │   └── ubuntu-browsers.d          10.4 Kb
│   ├── cache                          1.1 Mb

PS /etc> $hierarchy[0] | Get-Member -MemberType NoteProperty, MemberSet

   TypeName: System.Management.Automation.PSCustomObject

Name              MemberType   Definition
----              ----------   ----------
PSStandardMembers MemberSet    PSStandardMembers {DefaultDisplayPropertySet}
CreationTime      NoteProperty datetime CreationTime=12/14/2021 6:16:05 PM
FullName          NoteProperty string FullName=/etc
Hierarchy         NoteProperty string Hierarchy=etc
LastAccessTime    NoteProperty datetime LastAccessTime=12/16/2021 12:40:41 AM
LastWriteTime     NoteProperty datetime LastWriteTime=12/14/2021 6:16:05 PM
Nesting           NoteProperty int Nesting=0
Parent            NoteProperty DirectoryInfo Parent=/
RawSize           NoteProperty double RawSize=358229
Size              NoteProperty string Size=349.83 KB
```
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Well-done (checked current GitHub version) however [PSTreeStatic]::SizeConvert(1152) returns 1.13 KB while [math]::Round(1152/1KB,2) is 1.12 \$\endgroup\$ Commented Feb 6, 2022 at 17:11
  • \$\begingroup\$ Thank you @JosefZ, I've updated the code on Github \$\endgroup\$ Commented Feb 6, 2022 at 17:50

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.