It’s frustrating that testing whether a given string BEGINSWITH
a particular substring (prefix) is so straightforward, yet the equivalent test that asks whether a particular substring prefixes the given string isn’t—as far as I’m aware—among the available string comparison operations.
Constructing a predicate that compares the string value for path
against each individual element within a collection is entirely possible, but there are syntactic restrictions that limit collections to being the first (left-hand side) operand in a binary operation. So, even though it feels like we should be able to do this:
NOT (path BEGINSWITH ANY %@)
we can’t. And, while there really should be a string comparison operator to test for prefixes like this:
NOT (ANY %@ PREFIXES path)
there isn’t. The closest we can get is this:
NOT (ANY %@ IN path)
which can also be written like this:
NONE %@ IN path
Since macOS file paths aren’t case-sensitive, it probably makes sense to perform the string comparisons on a similar basis:
NONE %@ IN[c] path
This will, indeed, filter out all the paths in skipFolders
and their respective subpaths, however, in the (albeit unlikely) situation where the target directory to be enumerated contains a directory subtree whose relative folder path matches any of the absolute folder paths in skipFolders
, then this entire subtree will also be filtered out of the final result.
The likelihood of this is small, but not zero. Nonetheless, if you’re happy with this limitation, then you can replace this:
with this:
set thePredicate to current application's NSPredicate's predicateWithFormat_("NONE %@ IN[c] path", skipFolders)
set folderContents to (folderContents's filteredArrayUsingPredicate:thePredicate)
An Alternative Method: SUBQUERY()
Another approach to do away with the AppleScript repeat
loop is to bury it within the predicate itself, which is effectively what the SUBQUERY()
operation does. It’s probably slightly less performant that the above method, but it might still be more performant that the AppleScript repeat
loop in the average case and produces identical results.
If you’re not familiar with the SUBQUERY()
expression, you can read about it here. But, it should be pretty easy to intuit how it works by seeing it used in a predicate format string to achieve what we want:
SUBQUERY(%@, $fp, path BEGINSWITH[c] $fp).@count == 0
-
%@
: The collection being iterated over by SUBQUERY()
passed as an argument to predicateWithFormat_()
, namely skipFolders
.
-
$fp
: This is the variable identifier used to refer to the individual element of the collection within the expression. It serves an equivalent function to that of aFolder
in the AppleScript repeat
loop from your original script.
-
path BEGINSWITH[c] $fp
: This is the condition expression that will test the string value of path
for the elements in the array to be filtered against each individual element from the collection passed to SUBQUERY()
via %@
.
The important thing to note is that SUBQUERY()
is, itself, performing a filtering operation on the supplied array, i.e. skipFolders
. It, thus, returns a collection, which will be those elements of skipFolders
for which the condition expression is true. Since we only want those elements in the folderContents
array that fail the test for each and every element of skipFolders
, we need the collection returned by the SUBQUERY()
expression to contain zero items.
As before, the repeat
loop from your original script can be replaced with this:
set thePredicate to current application's NSPredicate's predicateWithFormat_("SUBQUERY(%@, $fp, path BEGINSWITH[c] $fp).@count == 0", skipFolders)
set folderContents to (folderContents's filteredArrayUsingPredicate:thePredicate)
I’ll let you evaluate timings for this.