Can AppleScript get folder contents with file extension exclusion

applescriptfinderfolders

Looking for a way in AppleScript to get the contents of a directory with an exclusion to parse through. I know how to do this using find in a for loop with the command:

find . -not -name "*.bat"

but I'm curious to know if there is an AppleScript only approach to getting a folder's contents without the file extension .bat.

I've built a dummy directory on my desktop, added several files and tried:

tell application "Finder"
    set theFiles to get every item of (entire contents of folder (choose folder)) whose kind ≠ ".bat"
    reveal theFiles
end tell

the .bat files are highlighted. Using does not contain compiles but errors out to:

Error:

Can’t get every item of true.

Code:

tell application "Finder"
    set theFiles to get every item of (entire contents of folder (choose folder) does not contain ".bat")
    reveal theFiles
end tell

Trying another approach I get an error of:

Error:

Finder got an error: Unknown object type.

Code:

tell application "Finder"
    set theFiles to get every item of (entire contents of folder (choose folder)) whose type ≠ {"public.bat"}
    reveal theFiles
end tell

If I try to modify the approach I can only get a solid result if I narrow down the name of a bat file using:

Code:

tell application "Finder"
    set theFiles to get (every file of folder (choose folder) whose name is not in {"foobar.bat"})
    reveal theFiles
end tell

In pure AppleScript is there a way to get contents of a directory with an exclusion for file type? I would possibly like to chain the exclusion for multiple file types.

Best Answer

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.