Wednesday, June 27, 2012

Powershell oneliner to find large files on all local drives

There are many different tasks that can be achieved with Windows Powershell on one line only (aka oneliner). One of this tasks is to retrieve all the largest files on all your local disks. Let's build together this script one step at the time.

First of all we have to get a list of the local hard drives. There are two ways to get a list of the drives on your local computer:

- Get-PSDrive cmdlet
- Get-WmiObject -class Win32_LogicalDisk

With Get-PSDrive all of you local and network locations are listed plus the registry, the certificate store and some other containers:

Name    Used (GB)  Free (GB) Provider      Root
----    ---------  --------- --------      ----
A                            FileSystem    A:\
Alias                        Alias
C           53,29      26,61 FileSystem    C:\
cert                         Certificate   \
D                            FileSystem    D:\
E             ,13      79,87 FileSystem    E:\
Env                          Environment
Function                     Function
HKCU                         Registry      HKEY_...
HKLM                         Registry      HKEY_...
Variable                     Variable
WSMan                        WSMan
X           14,29      25,61 FileSystem    X:\
Unfortunately the Provider (System.Management.Automation.ProviderInfo Provider) property doesn't make the difference between 'C' which is a local hard drive, and 'X', which is a network mapping. Both are reported as FileSystems.
Luckily, we can get this very same list by querying WMI data. For sake of simplicity I can add a multiple filter to restrict the results to disks C: and X: only:
Get-WMIObject -Query "SELECT * FROM Win32_LogicalDisk WHERE Name = 'C:' OR Name = 'X:'"

DeviceID     : C:
DriveType    : 3
ProviderName :
FreeSpace    : 28567015424
Size         : 85792387072
VolumeName   : System

DeviceID     : X:
DriveType    : 4
ProviderName : \\\x
FreeSpace    : 27493945344
Size         : 42842714112
VolumeName   : System
For the moment we could have the feeling that the difference between these drives is not shown neither. But this is false. In fact the DriveType property value is different: '3" means 'Local Drive' and '4' means 'Network drive'. There is also a valuable Description property to confirm this difference:
Get-WMIObject -Query "SELECT * FROM Win32_LogicalDisk WHERE Name = 'C:' OR Name = 'X:'" | select DeviceID, DriveType, Description

DeviceID  DriveType Description
--------  --------- -----------
C:        3         Local Fixed Disk
X:        4         Network Connection
What we've acquired now is the skill to find local drives by filtering on the DriveType property:
Get-WMIObject -class Win32_LogicalDisk -Filter 'drivetype=3' 

DeviceID     : C:
DriveType    : 3
ProviderName :
FreeSpace    : 28566921216
Size         : 85792387072
VolumeName   : System

DeviceID     : E:
DriveType    : 3
ProviderName :
FreeSpace    : 85756968960
Size         : 85896196096
VolumeName   : Data
This line of code can be shortened to:
gwmi Win32_LogicalDisk -Filter 'drivetype=3' 
Let's pass to the second part of this oneliner. Now we have to recursively run inside the two returned drives to find any file. This is achieved with the help of a ForEach loop, which is used to execute an action on every object returned from the class Win32_LogicalDisk. The action I am talking about in this case is Get-ChildItem. Get-ChildItem acts as ls or dir and lists items in specific locations.

To force Get-ChildItem to run recursively we have to add the '-recurse' switch and to force the execution of the listing from the root folder we must use a combination of a string concatenation and of a set of round parentheses:
Get-WMIObject -class Win32_LogicalDisk -Filter 'drivetype=3' | ForEach {Get-ChildItem ($_.deviceid + '\') -recurse}
Round parentheses are there to tell Powershell that I want to execute the Get-ChildItem cmdlet against the result of whatever is in between them. Being $_.DeviceID equal to C: and the concatenation of C: and '\' equal to 'C:\', Powershell will deductively execute 'Get-ChildItem C:\'.

This line can be written in a quicker way by using two aliases:
gwmi Win32_LogicalDisk -Filter 'drivetype=3'  | %{gci ($_.deviceid + '\') -recurse}
At this point we can pass to the third pass of the script. Which is where we refine our query to files larger then 100MB. Where-Object is our friend here. We just have to nest it inside the curly brackets of part two:
Get-WMIObject -class Win32_LogicalDisk -Filter 'drivetype=3' | ForEach {Get-ChildItem ($_.deviceid + '\') -recurse | Where-Object {$_.length -ge 100MB}}
The short version is:
gwmi win32_logicaldisk -Filter 'drivetype=3' | %{gci ($_.deviceid + '\') -recurse | ? {$_.length -ge 100MB}}
An additional steps is to make everything look nicer, and to do so we could rename the length property to Size (by using a 'Calculated property') and to create a table with only 5 columns: Name, Directory, Size, Creationtime and Lastwritetime. We can also go further and improve this by adding the information about the owner of these big files. This can be accomplished with the use of the Get-Acl cmdlet nested into Select operation. Last, to improve readability, we can sort the rows of this table by Size so to have the bigger files listed first.

The final script would then look like this:
Get-WMIObject -class Win32_LogicalDisk -Filter 'drivetype=3' | ForEach {Get-ChildItem ($_.deviceid + '\') -recurse | Where-Object {$_.length -ge 100MB}} | Select Name,Directory,@{label='Size';expression={($_.Length)}},CreationTime,LastWriteTime,@{label='Owner';expression={((Get-Acl $_.fullname).Owner)}} | Sort-object Size | Format-Table
... or, if you like the use of aliases:
gwmi win32_logicaldisk -Filter 'drivetype=3' | %{gci ($_.deviceid + '\') -recurse | ? {$_.length -ge 100MB}} | select name,directory,@{label='Size';expression={($_.length)}},creationtime,lastwritetime,@{label='Owner';expression={((get-acl $_.fullname).owner)}} | sort size | ft
This script works the same way on Powershell 2.0 and Powershell 3.0. I tested it on Powershell 1.0 also and got this error:
Select-Object : Illegal key label
Basically this error means that Powershell 1.0 doesn't accept the label property in the expressions. To solve this just replace label with name and it should run smoothly:
gwmi win32_logicaldisk -Filter 'drivetype=3' | %{gci ($_.deviceid + '\') -recurse | ? {$_.length -ge 100MB}} | select name,directory,@{name='Size';expression={($_.length)}},creationtime,lastwritetime,@{name='Owner';expression={((get-acl $_.fullname).owner)}} | sort size | ft
This is because the proper syntax to define a calculated property is:
@{name='calculated property name';expression={whatever calculation you want to make}
Powershell 2.0 and 3.0 are apparently less strict and allow you to use label instead of name, but you now know this is a syntax error.

I hope that I have been able to make my reasoning clear. If you want to suggest any improvement to this script, please do not hesitate to comment. Sharing information makes us stronger.


  1. Perfect! Just what I was looking for!
    Thanks for posting this information with explanations!

  2. Thanks for this and with nice Explanations!


Related Posts Plugin for WordPress, Blogger...