Better AppleScript Through Expressions

When writing a short AppleScript to complete a specific task, do not write a procedure that checks if A exists, then operates on it if it does. Instead, write an expression that operates on A, and ignore any exceptions caused by A not existing. Surrounding the statement in a try-block will ignore any exceptions it generates. The where and whose commands can help. This almost always leads to cleaner, and faster, AppleScripts.

Here is an example, a re-write of code in EyeTV version 2.5 (if you have it, you can find this script at EyeTV.app/Contents/Resources/English.lproj/EyeTV\ Help/openreadme.scpt).

When run, this opens a readme inside the EyeTV application bundle, iff the EyeTV application is running. I did not write this, and I do not know exactly how it is used, or what constraints it was written under — just what it does.

on run
set readmePath to "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd"

tell application "Finder"
 set allApps to every application process
 
 repeat with i from 1 to number of items in allApps
  set thisApp to item i of allApps
  
  if creator type of thisApp is "EyTV" then
   open file (((application file of thisApp) as string) & readmePath)
   exit repeat
  end if
 end repeat
end tell
end run

Re-writing the code by making it an expression that assumes the correct application process exists has a dramatic difference.

on run
try
 tell application "Finder"
  set theEyeTVProcessBundle to file of first item of (every application process where creator type is "EyTV") as string
  open (theEyeTVProcessBundle & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias
 end tell
end try
end run

We no longer need if or repeat blocks — they are procedural constructs that were used to find the correct application process, and guard against trying to open a file that does not exist. (I also removed the variable readmePath because I think the a path to “eyetvreadme.rtfd” is clearly a readme-path).

But it gets better. We could try removing the run and try blocks as well, and just ignore any errors the interpreter complains about. That brings the the script down to four lines. We can make it one-line: tell application "Finder" to open ((file of first item of (every application process where creator type is "EyTV") as string) & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias, but I think that makes the script too hard to understand, because the name “theEyeTVProcessBundle” documents the long expression needed to reliably find the application bundle of the running process.

One line or four lines, that’s still a big reduction from 13 lines. Now it’s short enough to embed directly in Cocoa code with NSAppleScript.

Here’s another example. This is from IMLocation‘s uninstaller code, it It removes the IMLocationHelper from the list of items that will be opened at login. Here’s the original code:

tell application "System Events"
 if (login item "IMLocationHelper") exists then
  delete login item "IMLocationHelper"
 end if
end tell

Applying the approach of re-writing the procedure as an expression

try
 tell application "System Events" to delete (login item "IMLocationHelper")
end try

Taking things a step further, by removing the try-block, and just ignoring errors when running the one-line script: tell application "System Events" to delete (get login item "IMLocationHelper"). This makes a big difference in the clarity of the surrounding Cocoa code.

AppleScript is a pretty poor language on it’s own. Usually I see it being used to get some specific thing done. In other words, generally AppleScripts are essentially expressions, used to get or set a single value; rarely are they sub-programs. This is whyExpressions are such a good fit.

A big thanks to Uli Kusterer for correcting my doubleplusungood code! (details in comments)

Advertisements
Explore posts in the same categories: AppleScript, Design, MacOSX, NSAppleScript, Programming, Research, Sample Code, Tips

2 Comments on “Better AppleScript Through Expressions”


  1. Originally I had rewritten the expression for the AppleScript from EyeTV as:
    tell application "Finder" to open((file of application process "EyeTV") as string) & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd" as alias

    To which Uli Kusterer commented

    Urk… that’s a bad rewrite. file of application process “EyeTV” is not the same as looking for an application with creator type “EyTV”. On most Macs it may be the same, but if a user has any app named “EyeTV” that isn’t really EyeTV, this will break. Not likely to happen, but beginning users can easily create duplicate names by accident.

    (But) the most important reason why your rewrite is doubleplusungood: Looking up the app by its creator type means it is found even if the user renames the application. Your code would break in that case.

    He’s absolutely right. I tried renaming EyeTV.app, and the code above failed. I shouldn’t have played so fast and loose with a different approach (name vs creator code). Because it was working in my limited testing (which didn’t involve renaming EyeTV.app) I got lazy and published it. I think one of the morals here is that if you’re rewriting an AppleScript, you should have a very solid understanding of any code you change.

  2. has Says:

    As a general rule, if you want to ensure portability when identifying applications, use creator type or bundle id, not filename. The AppleScript language fairly sucks at this at the moment (Leopard should finally address this deficiency), but it’s easily done if you’re using other approaches – appscript, NSWorkspace, LaunchServices, etc. However, if you’re stuck with AppleScript, there is a rather obscure and unintuitive way to locate application files by bundle id or creator type using the Finder, as in:


    tell application "Finder"
    open file "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd" of application file id "EyTV"
    end tell

    This code isn’t exactly the same as your original code either, of course, but I’m assuming that it isn’t really important if EyeTV is running or not when the script is called.

    That said, there is a further caveat here: if you’re writing scripts for public use, it’s a good idea to avoid Finder scripting if possible (e.g. use System Events instead) as not all OS X users run the Finder (e.g. Path Finder users) and may not appreciate your script launching it. This is pretty much a non-issue with languages other than AppleScript, which generally come with decent filesystem support already built in and/or can access the relevant system APIs directly. e.g. Assuming EyeTV is a Cocoa app and is running this script via NSAppleScript, it could easily and reliably achieve the same goals in just a couple of lines of ObjC:


    NSString *path = [[NSBundle mainBundle] pathForResource: @"eyetvreadme" ofType: @"rtfd"];
    BOOL success = [[NSWorkspace sharedWorkspace] openFile: path];

    Use the right tool for the job, etc.

    HTH

    has

    http://appscript.sourceforge.net
    http://rb-appscript.rubyforge.org


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: