Get Folders and Files Recursively with ASObjC

Thanks, Fredrik71, that worked.

Thanks. Yes, I was using Script Editor.

The following script searches a folder and its subfolders and returns files with specific file extensions. The timing result with a folder that contained 512 matching files out of 527 total files in 126 folders was 21 milliseconds:

use framework "Foundation"
use scripting additions

set theFolder to POSIX path of (choose folder)
set fileExtensions to {"pdf"} -- one or more file extensions not case sensitive
set theFiles to getFiles(theFolder, fileExtensions)

on getFiles(theFolder, fileExtensions)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set fileExtensions to (current application's NSArray's arrayWithArray:fileExtensions)'s valueForKey:"lowercaseString"
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() -- deep search that skips hidden files and package contents
	set thePredicate to current application's NSPredicate's predicateWithFormat_("pathExtension.lowercaseString IN %@", fileExtensions)
	return (folderContents's filteredArrayUsingPredicate:thePredicate) as list -- a list of file objects
	-- return ((folderContents's filteredArrayUsingPredicate:thePredicate)'s valueForKey:"path") as list -- a list of POSIX paths
end getFiles

If file extension cannot be used, the file’s type as determined by its Spotlight metadata will work, provided the drive is indexed. The timing result with my test folder was 60 milliseconds.

set theFolder to quoted form of POSIX path of (choose folder)
set theSearch to quoted form of ("kMDItemContentType == \"com.adobe.pdf\"")
-- set theSearch to quoted form of ("kMDItemKind == \"PDF document\"") -- a different approach
set theFiles to paragraphs of (do shell script "mdfind -onlyin" & space & theFolder & space & theSearch)

A somewhat common task is getting the contents of a folder sorted by some date. The following script returns regular files sorted by modification date:

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set theFiles to getFiles(sourceFolder)

on getFiles(theFolder) -- theFolder requires a POSIX path
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set fileKey to current application's NSURLIsRegularFileKey -- does not return packages
	set dateKey to current application's NSURLContentModificationDateKey
	set pathKey to current application's NSURLPathKey
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() -- options:7 will not recurse
	set theFiles to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aRegularFile} to (anItem's getResourceValue:(reference) forKey:fileKey |error|:(missing value))
		if aRegularFile as boolean is true then (theFiles's addObject:(anItem's resourceValuesForKeys:{dateKey, pathKey} |error|:(missing value)))
	end repeat
	theFiles's sortUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:dateKey ascending:true}
	return (theFiles's valueForKey:pathKey) as list -- returns a list of POSIX paths
end getFiles

The dateKey can be set to other values including:

NSURLAddedToDirectoryDateKey
NSURLAttributeModificationDateKey
NSURLContentAccessDateKey
NSURLCreationDateKey

The timing result with a folder that contained 534 files in 127 folders was 55 milliseconds. I tested numerous alternatives, but they were no faster.

The following script separately returns all folders and files in a specified folder and its subfolders. It is a minor refinement of my script in post 1. The timing result was 5 milliseconds when run on a folder containing 105 files in 11 subfolders and was 57 milliseconds when run on my home folder. Packages are returned with files.

If files or folders only are needed, this script remains the fastest alternative. For files, simply edit the last line of the handler to return theFiles. For folders, delete the last three lines of the handler and return theFolders.

The handler returns arrays of URLs, and these are coerced to a list of files, which can be used in a basic AppleScript.

use framework "Foundation"
use scripting additions

set theFolder to POSIX path of (choose folder)
set {theFolders, theFiles} to getFoldersAndFiles(theFolder)
set theFolders to theFolders as list
set theFiles to theFiles as list

on getFoldersAndFiles(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set directoryKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() --option 6 skips hidden files and package contents
	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
	set theFiles to folderContents's mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	return {theFolders, theFiles} --arrays of URLs
end getFoldersAndFiles
2 Likes

Here’s a script that uses Shane’s FileManagerLib, and runs just as fast, but is simpler to write, plus you get all the functionality of Shane’s library.

use framework "Foundation"
use scripting additions
use script "filemanagerlib"
set theFolder to path to documents folder as string
set theFolder to POSIX path of theFolder
set {theFolders, theFiles} to getFoldersAndFiles(theFolder)

on getFoldersAndFiles(aPath)
	set theFolders to objects of aPath ¬
		searching subfolders true ¬
		include folders true ¬
		include files false ¬
		result type paths list
	set theFiles to objects of aPath ¬
		searching subfolders true ¬
		include folders false ¬
		include files true ¬
		result type paths list
	return {theFolders, theFiles}
end getFoldersAndFiles

FileManager Lib and other appleScript libs and apps can be found here: Freeware | Late Night Software

Ed. I tested our scripts with Script Geek. The target was my home folder. My script took 74 milliseconds, and your script took 167 milliseconds. I edited the scripts to return files only, and the timing results were 72 milliseconds for my script and 98 milliseconds for your script. This testing assumes that the Foundation framework is in memory, which would normally be the case. I tested the scripts with smaller folders and received similar results.

BTW, I agree that Shane’s script libraries are great. I use them constantly.

When I tested them the first run was inconsistent, but when you remove that the timing was exactly the same.

Ed. That’s a mystery. I seem to recall that there was a significant speed bump in ASObjC a few versions of macOS back, and perhaps that’s the explanation (I’m on Sonoma). I retested just to make sure.

This is a variant of a script I posted in another forum. The script searches the specified folder and its subfolders and returns the most-recently modified file that contains a specified string in its file name. The timing result when run on my smallish home folder was 13 milliseconds.

The script uses the CONTAINS operator to make string comparisons, but the following can be substituted:

  • BEGINSWITH and ENDSWITH, both of which do what you would expect.
  • LIKE allows the use of * and ? wildcard characters.
  • MATCHES allows the use of a regular expression.

You can make these operators case and/or diacritic insensitive by appending [c], [d], or [cd] to the operator (e.g. CONTAINS[c]).

use framework "Foundation"
use scripting additions

set theFile to getFile("/Users/Robert", "Test") --the parameters are the target folder and search string

on getFile(theFolder, matchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set fileKey to current application's NSURLIsRegularFileKey
	set dateKey to current application's NSURLContentModificationDateKey --NSURLCreationDateKey if desired
	set pathKey to current application's NSURLPathKey
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() -- change options 6 to 7 to not descend into subfolders
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS %@", matchString)
	set filteredFiles to folderContents's filteredArrayUsingPredicate:thePredicate
	set sortedFiles to current application's NSMutableArray's new()
	repeat with anItem in filteredFiles
		set {theResult, aRegularFile} to (anItem's getResourceValue:(reference) forKey:fileKey |error|:(missing value))
		if aRegularFile as boolean is true then (sortedFiles's addObject:(anItem's resourceValuesForKeys:{dateKey, pathKey} |error|:(missing value)))
	end repeat
	sortedFiles's sortUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:dateKey ascending:false}
	set thePaths to (sortedFiles's valueForKey:pathKey)
	if thePaths's |count|() is 0 then return "No matching files found"
	return (thePaths's objectAtIndex:0) as text
end getFile
1 Like