Filtering Entire Contents Of

I have the following code:

    --Running under AppleScript 2.8, MacOS 13.0.1
on Finder_EntireContentsOf(input)
    try
        tell application "Finder"
            set input to (input as alias)
        end tell
        -- classes, constants, and enums used
        set NSDirectoryEnumerationSkipsHiddenFiles to a reference to 4
        set NSFileManager to a reference to current application's NSFileManager
        set NSDirectoryEnumerationSkipsPackageDescendants to a reference to 2
        set theURLs to (NSFileManager's defaultManager()'s enumeratorAtURL:input includingPropertiesForKeys:{} options:(NSDirectoryEnumerationSkipsPackageDescendants + (get NSDirectoryEnumerationSkipsHiddenFiles)) errorHandler:(missing value))'s allObjects()
        set allPosixPaths to (theURLs's valueForKey:"path") as list
    on error errorText number errorNumber partial result errorResults from errorObject to errorExpectedType
        if word 1 of errorText is (libraryName & "'s") then set errorText to "reports that " & errorText
        error libraryName & "'s <Finder_EntireContentsOf> " & errorText number errorNumber partial result errorResults from errorObject to errorExpectedType
    end try
end Finder_EntireContentsOf

From which I’d like to create variations but I don’t know how to best achieve these objectives.

Variations I’d like to implement:
filter: Files only
filter: Folders only
alter: Aliases returned rather than posix paths
filter: items whose name contains “someString”

I can achieve all of these by manipulating / filtering the results but I expect that there are superior methods. Can anyone help me with any of these? Where would I begin to learn more about the NSFileManager options available etc?

Download FileMangerLib of @Shane Stanley here: https://latenightsw.com/freeware/. Open it in the script editor to study. Everything is there. Learn the handlers that help you get and filter the entire contents of a folder. There are several of them which work together.

1 Like

Good advice, thanks! I also subsequently found a post here that address many of the filter options.

@paulskinner. The following script does what you want.

BTW, I ran a timing test with a source folder that contained 30 folders and 500 files. The script found 98 matches (3 folders and 95 files) and the timing result was 38 milliseconds.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set searchString to "somestring" -- search is case insensitive
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder, searchString)

on getFoldersAndFiles(sourceFolder, searchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set theKeys to {current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey}
	set theOptions to (current application's NSDirectoryEnumerationSkipsPackageDescendants as integer) + (current application's NSDirectoryEnumerationSkipsHiddenFiles as integer)
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:theKeys options:theOptions errorHandler:(missing value))'s allObjects()
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	set folderContents to folderContents's filteredArrayUsingPredicate:thePredicate
	set {theFolders, theFiles} to {{}, {}}
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
		if aDirectory as boolean is true then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
			if aPackage as boolean is false then
				set end of theFolders to anItem as alias
			else
				set end of theFiles to anItem as alias -- a package is considered a file
			end if
		else
			set end of theFiles to anItem as alias
		end if
	end repeat
	return {theFolders, theFiles}
end getFoldersAndFiles
1 Like

@peavine,

Indeed it does! Thank you very much! I’ll disect this and probably learn quite a bit.

1 Like

My script included above returns aliases as requested by @paulskinner. FWIW, the following returns files and can be made to return POSIX paths by enabling the last line of the handler. The timing result with my test folder of 30 subfolders and 500 files was 11 milliseconds.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set searchString to "somestring" -- search is case insensitive
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder, searchString)

on getFoldersAndFiles(sourceFolder, searchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set theKeys to {current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey}
	set theOptions to (current application's NSDirectoryEnumerationSkipsPackageDescendants as integer) + (current application's NSDirectoryEnumerationSkipsHiddenFiles as integer)
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:theKeys options:theOptions errorHandler:(missing value))'s allObjects()
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	set folderContents to folderContents's filteredArrayUsingPredicate:thePredicate
	set theFolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
		if aDirectory as boolean is true then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
			if aPackage as boolean is false then (theFolders's addObject:anItem)
		end if
	end repeat
	set theFiles to folderContents's mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	return {theFolders as list, theFiles as list} -- returns files
	-- return {(theFolders's valueForKey:"path") as list, (theFiles's valueForKey:"path") as list} -- returns paths
end getFoldersAndFiles
1 Like

@peavine,

Your last script is already very fast. As you may remember, in one of my posts earlier, I showed how you can improve the speed by 25-35% further. To do this, you need to avoid any coercions (boolean ones too) inside the repeat loops.

The following script in my tests improves your last script by about 33% in terms of speed. It’s not a very big improvement and you can ignore it, but still:
 

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (path to movies folder)
set searchString to "somestring" -- search is case insensitive
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder, searchString)

on getFoldersAndFiles(sourceFolder, searchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey} options:6 errorHandler:(missing value))'s allObjects()
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	set folderContents to folderContents's filteredArrayUsingPredicate:thePredicate
	set AsObjCTrue to current application's NSNumber's numberWithBool:true
	set theFolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
		if aDirectory is AsObjCTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
			if not (aPackage is AsObjCTrue) then (theFolders's addObject:anItem)
		end if
	end repeat
	set theFiles to folderContents's mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	return {theFolders as list, theFiles as list} -- returns files
	-- return {(theFolders's valueForKey:"path") as list, (theFiles's valueForKey:"path") as list} -- returns paths
end getFoldersAndFiles

 
Note:

  1. the smaller the search string “somestring” is, the more elements there will be in the repeat loop, and the greater the speed effect. I used the letter “a” for tests.

  2. I don’t have the strongest computer, so even a 25-35% improvement in script speed is noticeable and desirable.

2 Likes

@KniazidisR. I tested your suggestion, and it was about 7 percent faster on my 2023 Mac mini. If the number of folders and files is quite large, or if the computer is slow, the time savings can be significant. Thanks!

An interesting point is that your script uses an AppleScript operator to test equality of two Boolean NSNumber objects. I assume that the AppleScript Bridge intervenes and makes the necessary conversions. I thought a faster approach–which involves no bridging or coercions–would be the isEqual or isEqualToNumber methods, but that was slower in my testing.

FWIW, I’ve quoted below general advice from Shane on how to deal with booleans with ASObjC. Deciding what to use when can be difficult, and that’s probably why I almost always coerce a tested item to a boolean when checking for equality in a repeat loop. This has worked in every instance that I have encountered, and the speed penalty in many instances is small.

Your main use of BOOL will be like this:

  • When a method says it requires a BOOL, you can pass the AppleScript boolean true or false, and scripting bridge will take care of any conversion. You can also use the integers 0 and 1, or an NSNumber containing the value 0 or 1.
  • When a method says it will return a BOOL, the scripting bridge will normally intervene and pass you a boolean true or false, but may instead return an integer 1 or 0. You should test or coerce to make sure which it is.
  • If you have been returned a boolean value as an NSNumber, unless you are going to pass it to another method, you will need to coerce it to an AppleScript boolean using as boolean. You can also coerce it to an integer using as integer if that is more useful to you, in which case the result will be 0 or 1.

On a directory such as

"/Users/alan/Library/Mobile Documents/com~apple~CloudDocs/Documents/

the enumeratorAtURL appears to enumerate only those files which have been downloaded, and ignore those files which have not downloaded. The files not yet downloaded show an iCloud symbol with an underlying downward pointing arrow.
After I click the iCloud download icon, the enumeratorAtURL in the above AppleScript recognizes the url, and the script returns those additional files.
What method would you recommend to enumerate files which have not been downloaded but are listed in an iCloud directory?

I had forgotten there is an alternative approach (from Shane) which simplifies getting folders and files. It returns POSIX paths but is faster in my testing.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set searchString to "somestring" -- search is case insensitive
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder, searchString)

on getFoldersAndFiles(theFolder, searchString)
	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 NSURLPathKey, current application's NSURLIsPackageKey, current application's NSURLIsDirectoryKey}
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:theKeys options:6 errorHandler:(missing value))'s allObjects()
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	set folderContents to folderContents's filteredArrayUsingPredicate:thePredicate
	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
	set theFolders to (theFolders's valueForKey:(current application's NSURLPathKey))
	set theFiles to (folderContents's valueForKey:"path")'s mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	return {theFolders as list, theFiles as list}
end getFoldersAndFiles