Filter Reference Form

Filter Reference Form

Every once in a while my delight in the scriptable Finder is renewed, especially when I work with its support for the Filter reference form, also known as a whose clause. Here’s an example that dates from 1998, and which I update here to work in Mac OS X 10.4 (Tiger). It illustrates how much efficiency can be gained by using whose clauses instead of repeat loops, in applications that support whose clauses. Whose clauses are dramatically faster because it is the application itself that implements whose clauses, and the application knows a lot more about its internal data structures than AppleScript alone will ever know. Whose clauses also save a few lines, so they usually result in shorter scripts that are easier to understand.

The original exercise was conducted in the classic Mac OS, and I’ll include it here because Finder whose clauses achieved their greatest excellence in the classic Mac OS. Then I’ll modify the exercise so that it works in Mac OS X 10.4 (Tiger), and I’ll explain why it isn’t yet quite as satisfying as the classic Mac OS solution.

Way back in June 1998, Jon Pugh posted some nice little Finder scripts. One of them allowed you to select any file or files in the Finder’s front window, then automatically extend the selection to every file of the same type or types. It went like this, and it still works in Mac OS 9.2.2. I’ve changed the variable names and formatting to make it a little clearer.


tell application "Finder"
   activate
   
   set filters to (file type of every file of selection) as list
   
   set allFiles to {}
   repeat with thisFilter in filters
      set allFiles to allFiles & (every file of window 1 whose file type is thisFilter)
   end repeat
   
   select allFiles
end tell

Well, I wanted to change it a little so it would select folders or disks, too (Jon’s version doesn’t, because it asks explicitly for every file of selection), and to make it work with items on the desktop (Jon’s version doesn’t, because it asks explicitly for window 1). Then I got to thinking that any Finder script using a repeat loop is a candidate for a whose clause. Hmmm.

Here’s what I ended up with:


tell application "Finder"
   select (every item of container of selection whose file type is (file type of selection as list)) as list
end tell

Jon’s script was reduced to a single statement! And it works on the desktop because it uses ‘container of selection’ instead of ‘window 1’. And it allows selection of folders (file type ‘fold’, which unfortunately includes the trash on the desktop) and disks (file type ‘disk’) because it asks for ‘every item’ instead of ‘every file’. Best of all, it doesn’t have to waste time iterating through a repeat loop, but instead calls upon the power of a Finder whose clause to get every desired file in a single command.

This looks simple and maybe obvious, but here’s why it isn’t. Just like Jon’s script, it lets you select items having different file types, and it then selects all items in the container having any of those file types. Most surprising and pleasing of all, it works in the classic Finder with multi-level selections in any window that is in list view with its outline expanded. Select a file of one type at the root level of the outline, then shift-click on another item of another file type in an expanded suboutline in the same window to leave both selected. When you run the script, items of both types are selected at both levels of the outline. Cool!

Understanding why this works is the key to understanding the power of the filter reference form in the Finder. Don’t let the use of the singular instead of the plural of ‘container of’ and ‘file type of’ fool you. Both produce lists if multiple file types or items at different levels of an outline are selected, and the Finder knows how to cycle through all of them very rapidly. (Note, however, that the script will only select items at outline levels that contain selections when the script is run. If you select only a single item, similar items at other levels of the outline will not be selected.)

The downside to scripting the Finder with the filter reference form is that it can take a long time to figure out, and you are well advised to check out potential side effects. Furthermore, there have been subtle changes in the Finder’s execution of the filter reference form over time, so you have to test your scripts with every Finder version under which they are expected to run. Scripting with the filter reference form gets easier with practice, however, and the rewards are worth the effort if you rely heavily on Finder scripts.

Now for the Mac OS X 10.4 (Tiger) version. Things have changed a lot in Mac OS X and its file system since 1998, and in some respects the Finder hasn’t yet caught up. As a result, I have to resort to some techniques that weren’t necessary, or even available, in the classic Mac OS.

The most important change, which began in Mac OS X 10.3 (Panther) and has reached full flower in Tiger, is the transition from classic four-character file types to name extensions and, finally, Uniform Type Identifiers (UTI’s). In Tiger, it is no longer acceptable to rely on the File Type property of a Finder item, because many files created in newer applications simply don’t have a file type of the traditional kind. For a while, this gap could be filled by testing both for old-style file types and name extensions, but it is now much preferred to use UTI’s. Unfortunately, the Finder’s AppleScript dictionary doesn’t yet know about UTI’s.

Every file system object in Tiger has a UTI. The algorithm used to determine a file’s UTI takes into account all the old type information, if it is present, such as the four-character file type and the name extension, and it also takes into account new UTI metadata if newer applications have supplied it. This isn’t the place to go into details about UTI’s. Suffice it to say that they are hierarchical, allowing applications and scripts to determine a file’s specific file type, such as “public.jpeg” or “com.compuserve.gif”, or a general, more inclusive file type, such as “public.image”.

To use UTI’s in the Mac OS X version of Jon’s script and mine, I must therefore resort to the System Events application and also to the Info For command in the Standard Additions scripting addition, both of which are now UTI-savvy. I still have to use the Finder, too, however, because System Events only knows about the file system; it doesn’t know about the user interface, such as the Finder selection and which window is frontmost. Because I have to use the Finder, the System Events application and the Info For command, there are some tasks that can’t be performed by any one application’s implementation of whose clauses.

I’ll start with the Mac OS X version of Jon’s original classic Mac OS script. It is structured the same as his original script, but it can’t use the two whose clauses Jon used to work with a file’s File Type property. Instead, it has to use repeat loops and the Info For command to get a file’s UTI, expressed as a new Type Identifier property. Here it is:


tell application "Finder"
   activate
	
   set filters to {}
   repeat with thisFile in selection as list
      set end of filters to type identifier of (info for thisFile as alias)
   end repeat
	
   set allFiles to {}
   repeat with thisFilter in filters
      set thisFilter to contents of thisFilter -- dereference thisFilter
      repeat with thisFile in every document file in window 1 as alias list
         if type identifier of (info for thisFile) is thisFilter then set end of allFiles to thisFile
      end repeat
   end repeat
	
   select allFiles
end tell

If you look closely, you’ll see that there isn’t a whose clause anywhere in that script. Despite the issues raised by type identifiers, it nevertheless remains possible in Tiger to improve the script’s efficiency and make it shorter by using whose clauses. Here’s the Mac OS X version of my classic Mac OS script:


tell application "Finder"
   activate
	
   set thisFolder to folder of window 1 as alias
   set allFiles to {}
   repeat with thisFile in selection as list
      tell application "System Events"
         set typeID to type identifier of (info for thisFile as alias)
         set allFiles to allFiles & (every file of thisFolder whose type identifier is typeID)
      end tell
   end repeat
	
   select allFiles as alias list
end tell


Here’s an explanation of what’s going on.

The first several statements in the script, up to and including the repeat statement, are addressed to the Finder. This is necessary in order to get the Finder’s front window (in order to get its target folder, which the System Events application needs) and the Finder’s selection. The repeat loop is necessary in order to cycle through every file in a multiple Finder selection, in order to permit the System Events application to get each selected file’s UTI.

Then, within the repeat loop, I tell the System Events application to use a whose clause to cycle through the UTI’s of every file in the target folder all at once and compare them to the type identifier returned by the Info For command for the selected file I am currently examining. The whose clause can examine a large number of files in the target folder with great speed. The repeat loop won’t normally impose much of a speed penalty, because in normal usage users will probably select only one file to match.

Finally, I tell the Finder to select the list of matching alias references returned by the System Events application. This works for two reasons. First, the System Events application returns the matched files as a list of references in Alias reference form. Second, the Finder’s As Alias List coercion is able to convert this list of System Events-based alias references to a list of Finder-based alias references.

Unfortunately, the Mac OS X version of the script cannot extend the selection to multiple levels of an expanded outline view window, and it doesn’t work on the desktop, due to limitations in the Finder’s selection property in Mac OS X.

If you compare Jon’s original classic Mac OS script to my final Mac OS X script, you’ll see that they are virtually identical in structure and implementation. The only real difference is in my use of the System Events application to get file identifiers. It simply isn’t possible in Mac OS X at this time to achieve the efficiency of my classic Mac OS script. The reason is simple: the Mac OS X Finder doesn’t yet recognize type identifiers. Once it does, it should become possible to implement the classic Mac OS version of my script in Mac OS X, because it will no longer be necessary to resort to the System Events application and the Info For scripting addition. Full use of the Finder’s whose clause capability should become possible, once again allowing for a single-statement version of the script.