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
A script by Nigel in another thread prompted me to revisit an issue I investigated a few years back, which is how best to set NSURLResourceKeys when using the getResourceValue method. The results were the same as before:
-
pre-fetching the resource keys in the enumeratorAtURL and contentsOfDirectoryAtURL methods had no impact on the time it took the code to run; and
-
getting the resource keys inside of a repeat loop slows the script considerably.
I’ve included my testing below.
use framework "Foundation"
--SET KEY INSIDE OF REPEAT LOOP (117 MILLISECONDS)
set regularFiles to getFilesOne("/Users/robert/")
on getFilesOne(theFolder)
set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
set fileManager to current application's NSFileManager's defaultManager()
set theFiles to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()'s mutableCopy()
repeat with i from theFiles's |count|() to 1 by -1
set {theResult, aRegularFile} to ((theFiles's objectAtIndex:(i - 1))'s getResourceValue:(reference) forKey:(current application's NSURLIsRegularFileKey) |error|:(missing value))
if aRegularFile as boolean is false then (theFiles's removeObjectAtIndex:(i - 1))
end repeat
return theFiles as list
end getFilesOne
--SET KEY TO VARIABLE OUTSIDE OF REPEAT LOOP (84 MILLISECONDS)
set regularFiles to getFilesTwo("/Users/robert/")
on getFilesTwo(theFolder)
set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
set fileManager to current application's NSFileManager's defaultManager()
set fileKey to current application's NSURLIsRegularFileKey
set theFiles to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()'s mutableCopy()
repeat with i from theFiles's |count|() to 1 by -1
set {theResult, aRegularFile} to ((theFiles's objectAtIndex:(i - 1))'s getResourceValue:(reference) forKey:fileKey |error|:(missing value))
if aRegularFile as boolean is false then (theFiles's removeObjectAtIndex:(i - 1))
end repeat
return theFiles as list
end getFilesTwo
--SET KEY TO STRING VALUE INSIDE OF REPEAT LOOP (82 MILLISECONDS)
set regularFiles to getFilesThree("/Users/robert/")
on getFilesThree(theFolder)
set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
set fileManager to current application's NSFileManager's defaultManager()
set theFiles to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()'s mutableCopy()
repeat with i from theFiles's |count|() to 1 by -1
set {theResult, aRegularFile} to ((theFiles's objectAtIndex:(i - 1))'s getResourceValue:(reference) forKey:"NSURLIsRegularFileKey" |error|:(missing value))
if aRegularFile as boolean is false then (theFiles's removeObjectAtIndex:(i - 1))
end repeat
return theFiles as list
end getFilesThree
--PRE-FETCH KEY FOR EACH ITEM IN DIRECTORY (83 MILLISECONDS)
set regularFiles to getFilesFour("/Users/robert/")
on getFilesFour(theFolder)
set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
set fileManager to current application's NSFileManager's defaultManager()
set fileKey to current application's NSURLIsRegularFileKey
set theFiles to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{fileKey} options:6 errorHandler:(missing value))'s allObjects()'s mutableCopy()
repeat with i from theFiles's |count|() to 1 by -1
set {theResult, aRegularFile} to ((theFiles's objectAtIndex:(i - 1))'s getResourceValue:(reference) forKey:fileKey |error|:(missing value))
if aRegularFile as boolean is false then (theFiles's removeObjectAtIndex:(i - 1))
end repeat
return theFiles as list
end getFilesFour
BTW, a further reduction in timing results of about 8 percent can often be achieved by setting the Boolean NSNumber object outside the repeat loop (thanks KniazidisR). However, the documentation for the resource key needs to be consulted to see what it returns.
use framework "Foundation"
set booleanFalse to current application's NSNumber's numberWithBool:false -->(NSNumber) NO
Nigel’s script also reminded me that a filter can be used to get desired disk items. This script took 70 milliseconds to run and is functionally equivalent to those in the preceding post, except that this script returns paths and the others return file objects. BTW, the resource keys are pre-cached in this script, but this makes no difference in the timing result that I can ascertain.
use framework "Foundation"
set regularFiles to getFiles("/Users/robert/")
on getFiles(theFolder)
set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
set fileManager to current application's NSFileManager's defaultManager()
set fileKey to current application's NSURLIsRegularFileKey
set pathKey to current application's NSURLPathKey
set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{fileKey, pathKey} options:6 errorHandler:(missing value))'s allObjects() --option 6 skips hidden items and package contents
set fileData to current application's NSMutableArray's new()
repeat with anItem in folderContents
(fileData's addObject:(anItem's resourceValuesForKeys:{fileKey, pathKey} |error|:(missing value)))
end repeat
set thePredicate to current application's NSPredicate's predicateWithFormat_("%K == YES", fileKey)
(fileData's filterUsingPredicate:thePredicate)
return (fileData's valueForKey:pathKey) as list
end getFiles