Monday, September 21, 2020

Cleaning empty dirs with Powershell gci, hidden files like .DS_Store, and the power of the force

I saw this handy post recently using powershell's get-childitem (gci) to remove empty folders, which is great for truly empty directories with nothing in them. However, it didn't go into a few use cases where there are hidden or tiny files left in directories (hello mac SMB clients) that you might want to ignore when cleaning things up. I wanted to add on to that code as an exercise so read on for a few ways to solve this one if you don't have truly empty directories, but need to clean things up still.

I'm not a mac guy, but one thing I've come across is macs leaving behind little hidden or special files, in EVERY directory for some odd reason. (To be fair I guess PCs also do that as well with thumbnail cache files) These tiny files everywhere are painful for the system admin, as that adds to overhead for backups and devices serving the file systems, and in the above code example this can cause the directories to be seen as 'not empty' due to the index file or stub hanging out alone there.

Note: I'm on a linux host so the file paths are different, and using PS Core 7 in my testing as a mention. Also: Do you own code testing and validation of course, and feel free to use the below code at your own risk.

So to start with here's the original PS code from the link, which catches empty directories but would not catch a directory that has a single ".DS_Store" hidden file in it for example.
(gci “C:\dotnet-helpers\TEMP Folder” -r | ? {$_.PSIsContainer -eq $True}) | ?{$_.GetFileSystemInfos().Count -eq 0} | remove-item
The catch is that by default get-childitem (gci) doesn't return hidden files, which we don't care about initially perhaps, but later in the code you run a given directory through .getfilesysteminfos().count. When you use that getfilesystemsinfos().count it'll return the count of ALL files in the directory, which includes hidden files, which then would cause those directories found initially that are containing remnants or hidden .DS_store files to not be cleaned up/deleted. This is a problem if you have .DS_store files in every otherwise empty directory. The solution is to use the force.
"Don't underestimate the force" -Darth Vader
To begin with I just refactored the code into two lines to make it easier to read and work with later. I also updated it to catch the hidden files in the first gci by using the -force parameter. So after the first gci using -force it uses the list of all directories found, and will look through each directory returned to count and remove only directories with no files whatsoever, hidden or not. This is the same end-result as the first snippet, we're just using a second gci instead of the .getfilesysteminfos().count so we can have flexibility in the following examples. (you can remove the whatif to really delete directories, and add a -confirm:$false to not prompt if desired)

Delete only completely empty directories:
$dirs = gci "/home/dandill/test" -r -Force | ? {$_.PSIsContainer -eq $True} foreach ($d in $dirs){if((gci $d -force).count -eq 0){$d | remove-item -whatif}}
Then if we want to remove the directories without any regular files but that still contain hidden files we can do that like so. (note the lack of a -force on the second line, which then doesn't count hidden files in the logic to evaluate a directory on if it's considered 'empty')

Delete all directories with no regular files, and still delete them if they have any hidden files:
$dirs = gci "/home/dandill/test" -r -Force | ? {$_.PSIsContainer -eq $True} foreach ($d in $dirs){if((gci $d).count -eq 0){$d | remove-item -whatif}}
Lastly, if we had a bunch of empty directories, some with only hidden files named ".DS_Store" in them we could also delete those by specifically excluding them in the second gci with the -exclude parameter. Basically saying to look for all files in directories, but to not look at .DS_Store files for the second gci, and if you come up with zero results from that, then delete that directory.

Delete all directories that are completely empty, or contain only the ".DS_Store" file:
$dirs = gci "/home/dandill/test" -r -Force | ? {$_.PSIsContainer -eq $True} foreach ($d in $dirs){if((gci $d -exclude ".DS_Store" -force).count -eq 0){$d | remove-item -whatif}}
You can modify that exclude parameter in the second line with wildcards or as desired of course if you have other files hanging out causing your directories to not be considered to be empty.

So there you have it, use the -force if you want to clean up directories with PS and gci but may have a hidden file or two preventing that with defaults. Hope this is helpful or useful to you!