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
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