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 -ForceGets the hierarchy and folder size, including hidden ones, of theuserdirectory with a maximum of 10 levels of recursion.Get-PSTree /home/user -Deep: Gets the hierarchy and folder size of theuserdirectory 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
```
[PSTreeStatic]::SizeConvert(1152)returns1.13 KBwhile[math]::Round(1152/1KB,2)is1.12… \$\endgroup\$