PowerShell Get-DiskUsage CmdLet: how to list from

2019-03-22 08:17发布

问题:

I'm a relative newbie on PowerShell, wanted to know a bit more about functions, CmdLet and human readable values. Usually for learning new things, looking at what others do works well.

So I went looking and bumped into the source for a Get-DiskUsage CmdLet.

I can . source this into PowerShell, then call the function.

Somehow, it always uses the current directory for getting the results no matter how I call it.

What am I missing that it will not take the given -Path parameter?

PS D:\bin> . .\DiskUsage.ps1
PS D:\bin> Get-DiskUsage -Path D:\ -h

Size Folder
---- ------
405M


PS D:\bin> Get-DiskUsage -Path C:\ -h

Size Folder
---- ------
405M


PS D:\bin> C:
PS C:\> Get-DiskUsage -Path C:\ -h

Size Folder
---- ------
 18G


PS C:\>

The script output is not right, as this is what the SysInternals DiskUsage tool du shows:

D:\bin>du C:\

Du v1.4 - report directory disk usage
Copyright (C) 2005-2011 Mark Russinovich
Sysinternals - www.sysinternals.com

Files:        93367
Directories:  22541
Size:         21.817.875.778 bytes
Size on disk: 22.127.992.832 bytes


D:\bin>du D:\

Du v1.4 - report directory disk usage
Copyright (C) 2005-2011 Mark Russinovich
Sysinternals - www.sysinternals.com

Files:        132832
Directories:  15125
Size:         130.137.231.457 bytes
Size on disk: 54.992.396.288 bytes


D:\bin>du D:\bin

Du v1.4 - report directory disk usage
Copyright (C) 2005-2011 Mark Russinovich
Sysinternals - www.sysinternals.com

Files:        3118
Directories:  222
Size:         424.866.944 bytes
Size on disk: 288.858.112 bytes

回答1:

Get-DiscUsage C:\ # NOT RECOMMENDED

Get-DiskUsage -Path C:\Users\JBennett\SkyDrive\WindowsPowershell\Scripts\IO\

Size Folder
---- ------
223424B C:\Users\JBennett\SkyDrive\WindowsPowershell\Scripts\IO\DU

Either way works for me (after first dot-sourcing the script, as you said).

I have to say though, I wouldn't use that... It's doing a recursive directory scan with PowerShell, generating (thousands of) DirectoryInfo objects and then it's doing ANOTHER recursive directory scan ON EACH FOLDER, creating a new object and summing properties ... It's hideously slow, and will consume a lot of memory and IO compared to du.exe

A slightly better way

When you want the SIZE of every folder in a tree, recursively, the algorithmically correct way to do that DOES NOT involve the -Recurse flag on Get-ChildItem. You might do something like this:

function Get-Size {
  #.Synopsis
  #  Calculate the size of a folder on disk
  #.Description
  #  Recursively calculate the size of a folder on disk,
  #  outputting it's size, and that of all it's children,
  #  and optionally, all of their children
  param(
    [string]$root,
    # Show the size for each descendant recursively (otherwise, only immediate children)
    [switch]$recurse
  )
  # Get the full canonical FileSystem path:
  $root = Convert-Path $root

  $size = 0
  $files = 0
  $folders = 0

  $items = Get-ChildItem $root
  foreach($item in $items) {
    if($item.PSIsContainer) {
      # Call myself recursively to calculate subfolder size
      # Since we're streaming output as we go, 
      #   we only use the last output of a recursive call
      #   because that's the summary object
      if($recurse) {
        Get-Size $item.FullName | Tee-Object -Variable subItems
        $subItem = $subItems[-1]
      } else {
        $subItem = Get-Size $item.FullName | Select -Last 1
      }

      # The (recursive) size of all subfolders gets added
      $size += $subItem.Size
      $folders += $subItem.Folders + 1
      $files += $subItem.Files
      Write-Output $subItem
    } else {
      $files += 1
      $size += $item.Length
    }
  }

  # in PS3, use the CustomObject trick to control the output order
  if($PSVersionTable.PSVersion -ge "3.0") {
    [PSCustomObject]@{ 
      Folders = $folders
      Files = $Files
      Size = $size
      Name = $root
    }
  } else {
    New-Object PSObject -Property @{ 
      Folders = $folders
      Files = $Files
      Size = $size
      Name = $root
    }
  }
}

That way, you're not listing anything more than once. The down side is that you have to add everything up yourself, because you can't just use Measure-Object.

The script referenced in the OP actually lists every item however many times it is deep. That is, if a file is in C:\Users\Jaykul\Documents\WindowsPowerShell\Scripts ... it will show up in the gci -recurse for each parent folder -- five times if you call the script on the root.

Of course, you're still creating a FileInfo for every file and a DirectoryInfo for every directory, plus the summary PSObject I created by hand in the script, so even if this was nearly as fast as du.exe, it would have significantly more impact on memory.

However, that's the inherent trade-off in PowerShell: it's easier to do things... because we have heavy objects. We gladly trade ease of use for memory, and sometimes even a performance hit. That is to say: yes, du.exe will be faster, but it's a one-trick pony that someone had to specifically write for one purpose. It's still the best way, if all you wanted to do is run it and read the output. But it produces text output you have to parse if you want to use it's output in a script, and it's really hard to collect that information as one part of the output of an overall dashboard-style monitoring script...



回答2:

I am the creator of the Get-DiskUsage script. I have honestly no idea why it does not take the -Path for you. Works fine for me. Really wierd. Quick question though - did you try typing the path as D: instead of D:\?

@Jaykul - Very nice alternative way of doing it. I must admit that I did not know about the memory hit of using recursion in recusion. I always credited the slowness to the fact that the $results does not get outputted until all the other caclulations are complete. The reason I did a recursion and then again is simple - the first recursion lists all the folders from the rootpath (will make the list) and the second one I noticed that I needed because If I didn't the result would be a size that was not always accurate



回答3:

I liked Jaykul's answer, but it has a bug in PS 2.0 which returns the wrong answer when a directory is empty. I also modified it to support a force switch and a depth switch instead of recursive, similar to the original Get-DiskUsage scripts.

function Get-Size {
    #.Synopsis
    #  Calculate the size of a folder on disk
    #.Description
    #  Recursively calculate the size of a folder on disk,
    #  outputting it's size, and that of all it's children,
    #  and optionally, all of their children
    param(
        [string]$root=$PWD,
        # Show the size for each descendant recursively (otherwise, only immediate children)
        [Int]$Depth=1,
        [Switch]$Force
    )
    # Get the full canonical FileSystem path:
    $root = Convert-Path $root
    $DetailDepth = $Depth - 1
    $size = 0
    $files = 0
    $folders = 0
    $items = Get-ChildItem $root -Force:$Force
    foreach($item in $items) {
        if($item.PSIsContainer) {

            # Clear subItem in case the directory is empty.
            $subItem = @{ Folders = 0
                Files = 0
                Size = 0
                Name = $item.FullName }


            # Call myself recursively to calculate subfolder size
            # Since we're streaming output as we go, 
            #   we only use the last output of a recursive call
            #   because that's the summary object
            if($DetailDepth -ge 1) {
                Get-Size $item.FullName -Depth $DetailDepth -Force:$Force | Tee-Object -Variable subItems
                if($subItems.GetType().FullName -eq "System.Management.Automation.PSCustomObject") {
                    $subItem = $subItems
                } else {
                    $subItem = $subItems[-1]
                }
            } else {

                $subItem = Get-Size $item.FullName -Force:$Force | Select -Last 1
            }
            # The (recursive) size of all subfolders gets added
            $size += $subItem.Size
            $folders += $subItem.Folders + 1
            $files += $subItem.Files
            # avoid duplication by letting the parent report the summary values, except at the lowest detail depth
            if( $DetailDepth -lt 1) { 
                Write-Output $subItem
            }
        } else {
            $files += 1
            $size += $item.Length
        }
    }

    # in PS3, use the CustomObject trick to control the output order
    if($PSVersionTable.PSVersion -ge "3.0") {
        [PSCustomObject]@{ 
            Folders = $folders
            Files = $files
            Size = $size
            Name = $root
        }
    } else {
        New-Object PSObject -Property @{ 
            Folders = $folders
            Files = $files
            Size = $size
            Name = $root
        } 
    }
}