Can AppleScript get folder contents with file extension exclusion?
Solution 1:
Finder
Here are a few different ways to filter by file extension, with a brief description of their benefits. For the purposes of illustration, I've had Finder operations performed upon an open Finder window (my Pictures folder), which has many jpegs in it that serve as a good sample against which to test exclusion filters.
tell application "Finder" to select (every file in the ¬
front Finder window whose name does not end with ".jpg")
This took 14 seconds on its second run, to provide a baseline for rough comparisons.
The one simple act of coercing the result to an alias list
consistently sped up the performance time to 8 seconds (almost twice as fast):
tell application "Finder" to select ((every file in the ¬
front Finder window whose name does not end with ".jpg") as alias list)
Surprisingly to me, filtering by name extension
didn't seem to affect performance times compared to using the name
property, possibly suggesting both are performing equivalent string comparisons under the hood:
tell application "Finder" to select ((every file in the ¬
front Finder window whose name extension is not "jpg") as alias list)
Nonetheless, I would preferentially choose to filter by name extension
as a matter of personal choice, because it's cleaner syntax.
One benefit of Finder is that we can condense multiple filters into one, inclusive expression that essentially forms a list of predicates:
tell application "Finder" to select ((every file in the ¬
front Finder window whose name extension is not in ¬
["jpg", "png"]) as alias list)
and this was just as fast for two extensions than it was for one, and then again for four:
tell application "Finder" to select ((every file in the ¬
front Finder window whose name extension is not in ¬
["jpg", "png", "mov", "mp4"]) as alias list)
Filtering by the name
property can't be done with multiple extensions in this manner, so must be done with separate clauses for each extension you wish to exclude:
tell application "Finder" to select ((every file in the front Finder window whose ¬
name does not end with "jpg" and ¬
name does not end with "png" and ¬
name does not end with "mp4" and ¬
name does not end with "mov") as alias list)
System Events
System Events is much more equipped to handle file processing and filtering than Finder, which is somewhat counterintuitive. It also prevents Finder being blocked during the operation.
You cannot use a list of predicates as you can with Finder, so we have to make do with the longer expressions:
tell application "System Events" to get path of every file in the ¬
pictures folder whose visible = true and ¬
name extension is not "jpg" and ¬
name extension is not "png" and ¬
name extension is not "mp4" and ¬
name extension is not "mov"
tell application "Finder" to select the result
But the fact that this operation takes approximately zero seconds makes it worthwhile. Note here that I asked System Events to return the path
to these items, simply so I could then pass the result to Finder and have it select them as before (this is also how you can use reveal
after a System Events call).
Objective-C
If you need to search huge folders or a nested list of folders, don't be tempted to use Finder's entire contents
property, or it will make you cry. Use AppleScriptObjC, which performs deep searches of folder trees with performance times that are faster than System Events and shell. It comes at a cost of slightly more overhead compared to System Events/Finder, but probably comparable to that of calling a shell script.
First, it's best to define some handlers to take care of the file searching and filtering:
use framework "Foundation"
use scripting additions
property this : a reference to current application
property NSDirectoryEnumerationSkipsHiddenFiles : a reference to 4
property NSDirectoryEnumerationSkipsPackageDescendants : a reference to 2
property NSFileManager : a reference to NSFileManager of this
property NSMutableSet : a reference to NSMutableSet of this
property NSPredicate : a reference to NSPredicate of this
property NSSet : a reference to NSSet of this
property NSString : a reference to NSString of this
property NSURL : a reference to NSURL of this
on filesInDirectory:fp excludingExtensions:exts
local fp, exts
set all to contentsOfDirectory at fp
set |*fs| to all's filteredArrayUsingPredicate:(NSPredicate's ¬
predicateWithFormat:("pathExtension IN %@") ¬
argumentArray:[exts])
set fs to NSMutableSet's setWithArray:all
fs's minusSet:(NSSet's setWithArray:|*fs|)
fs's allObjects() as list
end filesInDirectory:excludingExtensions:
on contentsOfDirectory at fp
local fp
set FileManager to NSFileManager's defaultManager()
set fs to FileManager's enumeratorAtURL:(NSURL's ¬
fileURLWithPath:((NSString's stringWithString:fp)'s ¬
stringByStandardizingPath())) ¬
includingPropertiesForKeys:[] ¬
options:(NSDirectoryEnumerationSkipsHiddenFiles + ¬
NSDirectoryEnumerationSkipsPackageDescendants) ¬
errorHandler:(missing value)
fs's allObjects()
end contentsOfDirectory
Then, you can perform a call like this:
get my filesInDirectory:"~/Pictures" excludingExtensions:["jpg", "JPG", ""]
-- tell application "Finder" to reveal the result
Note that Objective-C will perform case-sensitive searches, so it's prudent to include file extensions to be excluded in both upper- and lowercase formats. The empty string excludes folders (except when the folder name contains a period).
Objective-C performed a deep enumeration of my Pictures folder filtering out all jpegs to return a list of 643 files nested within that directory, and it did this in approximately zero seconds. Neither Finder nor System Events can match this time (Finder won't respond after a while, and doing a deep search with System Events requires manually iterating through child folders, and deciding how to handle the nested list it returns, but it does it in an impressive 4 seconds). The shell is just as fast for this number of files, but I am fairly confident from what I've read that Objective-C will out perform the shell's find
command for large volumes of files.
You'll see I commented out the Finder's reveal
command: asking Finder to reveal 643 files in different folders was...um...unpleasant. I don't recommend it.
Conclusion
① Utilise System Events to perform file searches and optionally retrieve file paths, which can be utilised for any operations you wish Finder to subsequently perform upon them.
② If you do, for some reason, need to use Finder to perform the file search, always coerce to alias list
for performance benefits and a class of item that's universally much easier to handle later.
③ For large or nested folder structures, use Objective-C.
Solution 2:
Try this :
tell application "Finder"
set theFiles to files in folder (choose folder) whose name does not end with ".bat"
reveal theFiles
end tell