Removing Empty Folders from the Hierarchy of a selected Folder

Such a simple task as removing empty subfolders from the hierarchy of a selected folder is difficult for many users (a very frequent request for help). Especially, this concerns the recursive removal of empty subfolders throughout the depth of the hierarchy. Therefore, I wrote the following script using Finder.

It doesn’t pretend to be fast, but it works with visible structure elements (as I wanted). It allows selecting multiple root folders as well:


tell application "Finder" to set aFolders to (selection)
repeat with aFolder in aFolders
	my deleteEmptyFolders(aFolder)
	try
		tell application "Finder" to if count (items of aFolder) = 0 then delete (contents of aFolder)
	end try
end repeat


-- Direct recursion to remove empty subfolders.
on deleteEmptyFolders(aFolder)
	tell application "Finder" to set visibleItemsCount to count (items of aFolder)
	if visibleItemsCount = 0 then
		my deleteEmptyContainer(aFolder)
		try
			tell application "Finder" to delete (contents of aFolder)
		end try
	else
		tell application "Finder" to set aFolders to folders of aFolder
		repeat with aFolder in aFolders
			my deleteEmptyFolders(aFolder)
		end repeat
	end if
end deleteEmptyFolders


-- Reverse recursion to remove emptied containers.
on deleteEmptyContainer(aFolder)
	tell application "Finder"
		try
			set aFolder to container of aFolder
			set visibleItemsCount to count (items of aFolder)
			if visibleItemsCount = 1 then
				delete aFolder
				my deleteEmptyContainer(aFolder)
			end if
		end try
	end tell
end deleteEmptyContainer

Thanks @KniazidisR for the script.

The following script identifies (but does not delete) empty folders and writes them to a text file on the desktop. The text file does not include folders that contain empty folders and nothing else (it probably should). The script is reasonably quick, taking 60 milliseconds to work on my home folder.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set theFolders to getFolders(sourceFolder)
set emptyFolders to getEmptyFolders(theFolders)
set textFile to POSIX path of (path to desktop) & "Empty Folders.txt"
writeFile(textFile, emptyFolders)

on getFolders(theFolder) -- a rewrite of a handler by Shane
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set theKeys to {current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey, current application's NSURLPathKey}
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:theKeys options:6 errorHandler:(missing value))'s allObjects() -- option 6 skips hidden files and package descendants
	set theFolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		(theFolders's addObject:(anItem's resourceValuesForKeys:theKeys |error|:(missing value)))
	end repeat
	set thePredicate to current application's NSPredicate's predicateWithFormat_("%K == YES AND %K == NO", current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey)
	set theFolders to theFolders's filteredArrayUsingPredicate:thePredicate
	return (theFolders's valueForKey:(current application's NSURLPathKey))
end getFolders

on getEmptyFolders(theFolders)
	set theFileManager to current application's NSFileManager's defaultManager()
	set emptyFolders to current application's NSMutableArray's new()
	repeat with aFolder in theFolders
		set aFolder to (current application's |NSURL|'s fileURLWithPath:aFolder)
		set subfolderContents to (theFileManager's contentsOfDirectoryAtURL:aFolder includingPropertiesForKeys:{} options:4 |error|:(missing value)) -- option 4 skips hidden files
		set subfolderContentsCount to subfolderContents's |count|()
		if subfolderContentsCount = 0 then (emptyFolders's addObject:aFolder)
	end repeat
	set emptyFolders to (emptyFolders's valueForKey:"path")'s sortedArrayUsingSelector:"localizedStandardCompare:"
	return (emptyFolders's componentsJoinedByString:linefeed)
end getEmptyFolders

on writeFile(theFile, theText)
	(current application's NSString's stringWithString:theText)'s writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end writeFile
1 Like

@peavine,

Your code works great and might come in handy. Only, as I notice not for the first time, you use too long forms of notation for some commands. This is not very important, but why write long where it can be written shorter.

For example, you can do this:

set theKeys to {"NSURLIsDirectoryKey", "NSURLIsPackageKey", "_NSURLPathKey"}

and this:

set thePredicate to current application's NSPredicate's predicateWithFormat:"NSURLIsDirectoryKey == YES AND NSURLIsPackageKey == NO"

and this:

return theFolders's _NSURLPathKey

and this:

set emptyFolders to (emptyFolders's |path|)'s sortedArrayUsingSelector:"localizedStandardCompare:"

Also, you can use keyword “my” instead of “current application’s”

KniazidisR. Thanks for looking at my script.

The getFolders handler was originally from Shane, and I’m not sure why current application is (or isn’t) required with an NSURLResourceKey. Just by way of example, see Shane’s post here and look at page 142 of his ASObjC book. As regards valueForKey, I disagree that omitting this is a good idea, but that’s just my personal opinion.

BTW, I was interested by Shane’s comment:

Interestingly I could never discern any difference with or without the includingPropertiesForKeys: value.

Nigel makes some interesting comments on this point, and I’ll have to run some timing tests to see what works best for me. I like this approach to getting folders, files, and packages and optimizing it would be worth the effort. The only downside is that it returns paths.

The following is an optimized version of my script.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set theFolders to getFolders(sourceFolder)
set emptyFolders to getEmptyFolders(theFolders)
set textFile to POSIX path of (path to desktop) & "Empty Folders.txt"
writeFile(textFile, emptyFolders)

on getFolders(sourceFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set directoryKey to (current application's NSURLIsDirectoryKey)
	set packageKey to (current application's NSURLIsPackageKey)
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() -- option 6 skips hidden files and package descendants
	set theFolders to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true -- thanks KniazidisR
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:directoryKey |error|:(missing value))
		if aDirectory is booleanTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if not (aPackage is booleanTrue) then (theFolders's addObject:anItem)
		end if
	end repeat
	return theFolders
end getFolders

on getEmptyFolders(theFolders)
	set theFileManager to current application's NSFileManager's defaultManager()
	set emptyFolders to current application's NSMutableArray's new()
	repeat with aFolder in theFolders
		set subfolderContents to (theFileManager's contentsOfDirectoryAtURL:aFolder includingPropertiesForKeys:{} options:4 |error|:(missing value)) -- option 4 skips hidden files
		set subfolderContentsCount to subfolderContents's |count|()
		if subfolderContentsCount = 0 then (emptyFolders's addObject:aFolder)
	end repeat
	set emptyFolders to (emptyFolders's valueForKey:"path")'s sortedArrayUsingSelector:"localizedStandardCompare:"
	return (emptyFolders's componentsJoinedByString:linefeed)
end getEmptyFolders

on writeFile(theFile, theText)
	(current application's NSString's stringWithString:theText)'s writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end writeFile

FWIW, I ran this command (without delete) on my home folder, and it returned 3780 empty folders. These included hidden folders and folders in packages. Care needs to be exercised in running this command.

1 Like

But this script will not delete all empty folders, but only empty folders of the lowest level. If a folder contains only an empty subfolder, then after deleting it, this folder also becomes empty. But your script will leave it, that is, the work will not be completed to the end. My script removes all empty folders.

UPDATE: wrong comment.

what is the grep .txt part needed for? The whole command will only deliver a list of files with the extension .txt which are empty, as I see it. If that’s what you want, you could add another condition to the find call.

According to this

the script will work recursively. I can’t try that right now since I’m away from my Mac.

1 Like

@Fredrik71,

I apologize for my previous comment because it is wrong. It turns out that your laconic script works correctly and deletes empty folders of all levels. I checked like this:

 

set rootFolder to POSIX path of (path to desktop) & "Untitled folder"
set rootFolder to quoted form of rootFolder

do shell script "find " & rootFolder & " -empty -type d  -delete"

 

Something like
find … -a -name "*.txt"