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
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
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”
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.
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.
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.
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"