Display friendly file sizes in PowerShell


At one of my recent classes, while discussing hashtables and calculated properties I showed an example of how to list the file’s size in kilobytes instead of the default bytes size.

This can be accomplished with {$_.Length/1kb} as the expression in the property’s hash, but then the output can be… not too pretty… and from some reason, the folders have a non-zero size:

PS> dir | Select-Object -Property Mode, LastWriteTime, @{N='SizeInKb';E={$_.Length/1kb}}, Name

Mode   LastWriteTime              SizeInKb Name
----   -------------              -------- ----
d----- 20/05/2017 20:10:51    0.0009765625 Logs
-ar--- 03/01/2017 15:58:42     3.041015625 differentfile.exe
-ar--- 30/11/2016 09:40:47    0.2705078125 file.txt
-a---- 21/03/2017 19:46:16           12044 large.msi
-a---- 21/03/2017 19:46:18 2902.8388671875 medium.pkg
-a---- 20/05/2017 21:02:19    1.2568359375 otherfile.txt

A possible solution to this, can be truncating some numbers after the decimal point, and then casting it to a double to still be able to sort the files by their size:

PS> dir | Select-Object -Property Mode, LastWriteTime, @{N='SizeInKb';E={[double]('{0:N2}' -f ($_.Length/1kb))}}, Name | Sort-Object -Property SizeInKb

Mode   LastWriteTime       SizeInKb Name
----   -------------       -------- ----
d----- 20/05/2017 20:10:51        0 Logs
-ar--- 30/11/2016 09:40:47     0.27 file.txt
-a---- 20/05/2017 21:02:19     1.26 otherfile.txt
-ar--- 03/01/2017 15:58:42     3.04 differentfile.exe
-a---- 21/03/2017 19:46:18  2902.84 medium.pkg
-a---- 21/03/2017 19:46:16    12044 large.msi

But why only in kilobytes? Why not have the size display in human readable format?

So this brought me to write the Get-FriendlySize function:

function Get-FriendlySize {
    param($Bytes)
    $sizes='Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ','
    for($i=0; ($Bytes -ge 1kb) -and 
        ($i -lt $sizes.Count); $i++) {$Bytes/=1kb}
    $N=2; if($i -eq 0) {$N=0}
    "{0:N$($N)} {1}" -f $Bytes, $sizes[$i]
}

So I could call it with the Select-Object cmdlet:

PS> dir | Select-Object -Property Mode, LastWriteTime, @{N='FriendlySize';E={Get-FriendlySize -Bytes $_.Length}}, Name

Mode   LastWriteTime       FriendlySize Name
----   -------------       ------------ ----
d----- 20/05/2017 20:10:51 1 Bytes      Logs
-ar--- 03/01/2017 15:58:42 3.04 KB      differentfile.exe
-ar--- 30/11/2016 09:40:47 277 Bytes    file.txt
-a---- 21/03/2017 19:46:16 11.76 MB     large.msi
-a---- 21/03/2017 19:46:18 2.83 MB      medium.pkg
-a---- 20/05/2017 21:02:19 1.26 KB      otherfile.txt

But wait! Now I can’t sort them. The FriendlySize is just a string:

PS> dir | Select-Object -Property Mode, LastWriteTime, @{N='FriendlySize';E={Get-FriendlySize -Bytes $_.Length}}, Name | Sort-Object -Property FriendlySize

Mode   LastWriteTime       FriendlySize Name
----   -------------       ------------ ----
d----- 20/05/2017 20:10:51 1 Bytes      Logs
-ar--- 03/01/2017 15:58:42 3.04 KB      differentfile.exe
-ar--- 30/11/2016 09:40:47 277 Bytes    file.txt
-a---- 21/03/2017 19:46:16 11.76 MB     large.msi
-a---- 21/03/2017 19:46:18 2.83 MB      medium.pkg
-a---- 20/05/2017 21:02:19 1.26 KB      otherfile.txt

Then it occurred me, why not just override the ToString method on the Length’s FileInfo object property when it’s displayed and leave the original length’s value as it was?

This can be accomplished by changing the format data for the object.

So I wrote some lines of code that read the default FileSystem.format.ps1xml file and change the property length to a scriptblock, and update the format data with the Update-FormatData cmdlet:

(Note the double dollar sings before the under-score ($$_) in the here-string, this is to avoid the substitution with the match group)

$file = '{0}myTypes.ps1xml' -f ([System.IO.Path]::GetTempPath()) 
$data = Get-Content -Path $PSHOME\FileSystem.format.ps1xml
$data -replace '<PropertyName>Length</PropertyName>', @'
<ScriptBlock>
if($$_ -is [System.IO.FileInfo]) {
    $this=$$_.Length; $sizes='Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ','
    for($i=0; ($this -ge 1kb) -and ($i -lt $sizes.Count); $i++) {$this/=1kb}
    $N=2; if($i -eq 0) {$N=0}
    "{0:N$($N)} {1}" -f $this, $sizes[$i]
} else { $null }
</ScriptBlock>
'@ | Set-Content -Path $file
Update-FormatData -PrependPath $file

After running the code, I can simply list the files and get their friendly sizes with sorting and everything!

PS> dir | Sort-Object -Property Length

    Directory: C:\TestDirectory

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-ar---       30/11/2016     09:40      277 Bytes file.txt
-a----       20/05/2017     21:02        1.26 KB otherfile.txt
-ar---       03/01/2017     15:58        3.04 KB differentfile.exe
-a----       21/03/2017     19:46        2.83 MB medium.pkg
-a----       21/03/2017     19:46       11.76 MB large.msi
d-----       20/05/2017     20:10                Logs

If you want to always have this, put the new myTypes.ps1xml file somewhere on your drive, and add the call to

Update-FormatData -PrependPath $file

in your profile.

HTH,

\Martin.

Comments (1)

  1. Beniro Mourelo says:

    In the example:
    PS> dir | Select-Object -Property Mode, LastWriteTime, @{N=’FriendlySize’;E={Get-FriendlySize -Bytes $_.Length}}, Name | Sort-Object -Property SizeInKb

    I think it should be : :
    PS> dir | Select-Object -Property Mode, LastWriteTime, @{N=’FriendlySize’;E={Get-FriendlySize -Bytes $_.Length}}, Name | Sort-Object -Property FriendlySize

Skip to main content