AppleScript
an Informal Overview

by Margaret Magnus

copyright by Margaret Magnus
all rights reserved


My Web Site
Margo's Magical Letter Page


I was assigned a project using AppleScript. The project was due on short term, and I had never used AppleScript, and had only a vague idea of what it did. I did, however, have experience with normal programming languages like C and Pascal and with scripting languages in, for example, database applications.

Normally, I learn languages by buying a sort of no nonsense text and reading it cover to cover. But time constraints precluded that in this case. I learned a number of things about AppleScript rather laboriously and too late that I could have learned easily up front. I think even had I had some leisure to learn the language properly, it would have been nice to know right up front the few things that I will write in this document. These are the kind of things that the pros assume you already know before you even start. I didn't know them.


Handy AppleScript Facts

1. AppleScript is a scripting language for the Macintosh, which means that it is used to automate repetitive tasks involving the system and (optionally) Macintosh applications.

2. You can download a full 406 page .pdf manual for AppleScript off Apple's technical documentation Web site for free. Or get a hard copy version here. It took me hours and hours to find this manual. You can't find it by doing a search at Apple's Web site, so make sure you bookmark the page. In most ways, I found this manual more helpful than books you can buy in the store, because the way I learn things is to first get a general idea about what it's all about. You get an idea of the scope and nature of AppleScript just by reading the table of contents of this manual. However, I also found it incomplete or at least obscure in many ways, and so I did end up using Applescript for Dummies, if you don't mind all the unnecessary words in the 'Dummies' series. For example, the pdf manual does not mention the 'choose file' command, which opens a dialogue box and allows you to choose a file to open from it. You can find these 'Scripting Additions' which aren't documented by the Apple pdf File (which I still haven't read cover to cover) in your system extensions folder under 'Scripting Additions'. Danny Goodman's Applescript Handbook. has also been recommended to me.

For Apple documentation, see also:

AppleScript Language
Scripting Additions
AppleScript Finder
See Also AppleScript SourceBook

3. Applescript runs on Macintosh system 7.5 and higher, and is supplied automatically with the system. You can just do a Find File on 'Applescript' to find the 'Script Editor' application that edits the scripts. The Applescript Folder on most systems is in the 'Apple Extras' folder.

4. There is a record button on a blank file that appears when you launch the Applescript script editor. If you have to do anything but the most basic Finder tasks, like disconnecting from PPP, this button is useless, because it understands nothing that happens within an application, but I found it helpful to record a few things in the Finder just to get an idea of what gets recorded when I do what.

5. In order to control anything with Applescript, it has to be 'Scriptable'. Some critical things which aren't scriptable through system 9.1 are the Find File or Sherlock commands and SimpleText. Also applications vary as to how scriptable they are. Some applications, like FileMaker, do quite a good job of making all its functions available to Applescript. Other applications implement the scripting badly. Sometimes you run commands that are supposed to be there and they're just not implemented. You can expect that the 'open' command you give to each application is implemented differently, and you can expect the commands to function differently. To find out what applications are scriptable, you can go to the Applescript Script Editor's 'Open Dictionary' command. Find the application you are interested in. If it appears, the application is scriptable. If you select that application, a dialog box listing the commands available will open for each scriptable application.

6. Applescript is a full blown language. It is interpreted. It is object oriented. You can do string and number manipulation. Because they try to make the syntax look natural language, and because of the 'record' command which is the first thing most people try, and because no Applescript documentation comes with your computer, this may pass you by for awhile. For this reason, if you, for example, want to read data from your e-mail and import it into a database, you can just read the e-mail and drop the data directly in the right fields using the string manipulation functions in Applescript. You don't have to do the string manipulation within the applications themselves. Also, don't let this natural languaging fool you. The syntax is just as strict as in any other language. You have to make sure you've type checked properly. The syntax checker doesn't check type matching, obviously, because each application has its own types. To check the syntax of a program, you punch the syntax button in the upper right hand corner of the document window.

7. Most applications that are scriptable either supply sample scripts when you install the application (for example Filemaker), or have places on the Web where they provide sample sample scripts. I found sample scripts to be even more important learning Applescript than learning C, because so much depends on learning how each application implements the commands. It's not enough to learn how Apple's Finder commands work. You have to learn how the commands for each application you are going use work. Often the only way to really understand how a given application opens a file or sends e-mail is to read a sample script, because it's not documented in any other way. You cannot learn this without looking at sample scripts supplied with each application.

8. It's really good to know the applications you are accessing with Applescript quite well. For example, I was not aware that I could send mail directly from FileMaker 4.0, so I spent a long time trying to figure out how to move the information from FileMaker back to Eudora so I could e-mail it... Had I but seen the 'send mail' command mentioned (only) under 'Miscellaneous commands' on page 5-11 of the chapter on scripting in the FileMaker manual, I would have saved myself hours and hours. This kind of issue is much more prevalent in Applescript, I find, than in programming other languages. It's like knowing the entire Macintosh toolbox before you try writing an application or knowing all the built-in functions before trying to write in C. Otherwise, you repeat things that are already done, and you do them in a way that's not natural to the flow.

9. I found the most difficult aspect of learning to program in Applescript was learning to make asynchronous events synchronous. For example, if you tell Eudora by means of Applescript to connect to PPP and then immediately follow that by a command that sends it to work with the messages in the in-box, it will not wait for the connection to complete before trying to read the e-mail in the in-box. It will instead try to read the e-mail while it is still in the process of connecting. You have to bear this in mind all the time as you're programming.




Commands

Following are a few especially handy commands. I present them in the order I needed them for my little application:


activate: launches an application or brings it to the front if it's already open.

Example:
activate application "Filemaker"

If you refer to an application in this way, then when you check the syntax of the script, Applescript will open a dialog box and ask you to locate the relevant application program. It then inserts the exact application name into your program.


tell: In order to start executing any command specific to an application, you have to put those commands between tell and end tell. You can nest 'tell' commands.

Example:

tell application Filemaker
do script "Sort Found Records"
end tell

If you don't put a Filemaker 'do script' command between the 'tell' and 'end tell', Applescript will first try to run the Finder's 'do script' command and will then look for an Applescript script by that name. It will not even look for a Filemaker script in the open file.


choose file: opens a dialog box so you can choose a file to open

Example:

tell application "Filemaker"
set new_file to choose file with prompt "Open file to be edited" of type ("FMP3")
open new_file
end tell


set to: is the way you assign things to other things. You don't use '='

Example:

set response_number to 0
if not read_mail(new_message) is 0
set response_number to response_number + 1
end if

Notice the use of not.


comments: are done with --

Example:

--The following lines import the e-mail message into a Filemaker record


if: The 'if' syntax is:

if ... then
...
else
...
end if


repeat: Loops are done with repeat

Example:

repeat
get_next_message(new_message)
import_data(new_message)
set query_number to query_number + 1
end repeat


subroutines: To call a subroutine, you just give the name of the subroutine with it parameters, much as you do in any other language. You use the on command to indicate the start of a subroutine.

Example:

if not import_data(x,y) = 0 then
....
else
display dialog "Error importing data"
end if
 
on import_data (m,n)
...
end import_data

NOTE: Subroutines cannot be called from within a tell ... end tell unless, you use 'my'. 'My' tells Applescript that you want to execute an Applescript command proper and not a command from the application you are addressing:

Example:

tell application "Eudora"
connect
if not my import_messages(x,y) is 0 then
...
else
end if
end tell
 
on import_messages(a,b)
...
end import_messages


try: Whenever something fails, Applscript's default response is to bail out of the script. To prevent the script from ending, you have to use try.

Example:

try
tell application "Filemaker"
set new_file to choose file with prompt "Open file to be edited" of type ("FMP3")
open new_file
end tell
on error
end try

If you don't put that try in there, the whole script will simply end if the user doesn't open a file, because, for example, it's already open.


lists: are arrays

Example:
set field_names to ¬
{{"Submission Date", "Time_Submitted"}, ¬
{"First Name", "First_Name"}, ¬
{"Last Name", "Last_Name"}, ¬
{"Company", "Company"}, ¬
{"Phone", "Phone"}, ¬
{"Fax", "Fax_Number"}, ¬
{"EMail", "Email"}, ¬
{"Address", "Address"}, ¬
{"City", "City"}, ¬
{"State", "State"}, ¬
{"Zip", "Zip_Code"}, ¬
{"Country", "Country"}, ¬
{"Industry Type", "Industry_Type"}, ¬
{"Other Industry", "Other_Industry"}, ¬
{"Mailing List", "Mailing_List"}, ¬
{"Comments", "Comments"}}

Note: Use option-return to spread a single command over several lines.


classes: AppleScript is object oriented. Every value has a class, and classes have elements and properties. An example of a class is 'database' in Filemaker, which has for elements layout and script, and for properties things like its name, whether its locked, the current layout, and so forth. You access the properties of a class with various prepositions. For example, you access the property 'body' of a Eudora message by using the syntax 'body of message'.


display dialog: allows you to put up a dialog box easily

Example:

set response_action to display dialog ¬
"How would you like to respond?" buttons {"No Reply", "Default", "Custom"} default button "Default"
if response_action is "Custom" then
try
do script "Edit Custom Reply"
on error
end try
else if response_action is "Default" then
try
do script "Send Standard Reply"
on error
end try
end if


Sample Applescript

This script takes data from Eudora e-mail, puts it into a Filemaker database, and then sends replies using a Filemaker script

activate application "Eudora Pro 4.1b 39"
-- get messages from Eudora
tell application "Eudora Pro 4.1b 39"
set mail_count to count message of mailbox "in"
set query_number to 0
if mail_count > 0 then
repeat with i from 1 to mail_count
try
set message_body to body of message i of mailbox "in"
on error
exit repeat
end try
try
set subject_field to field "subject" of message i of mailbox "in"
on error
end try
set paragraph_number to count paragraphs in message_body
if (subject_field is not "") and (subject_field contains "Feedback from www.draco.com webform") then
set query_number to query_number + 1
if my import_data(message_body, paragraph_number) is 0 then
display dialog "failure at message number " & i
end if
end if
end repeat
end if
end tell
 
tell application "FileMaker Pro"
with timeout of 1200 seconds
if query_number = 0 then
display dialog "There were no new responses."
else
set user_response to display dialog "There were " & query_number & " new responses. ¬
Do you wish to reply now?" buttons {"yes", "no"} default button "no"
set button_name to button returned of user_response
if button_name is "Yes" then
do script "Send Replies"
end if
end if
end timeout
end tell
 
---------------
 
on import_data(new_message, paragraph_number)
set old_delimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to {"="}
--specify Filemaker and corresponding Web site fields names {Filemaker,Web}:
set field_names to ¬
{{"Submission Date", "Time_Submitted"}, ¬
{"First Name", "First_Name"}, ¬
{"Last Name", "Last_Name"}, ¬
{"Company", "Company"}, ¬
{"Phone", "Phone"}, ¬
{"Fax", "Fax_Number"}, ¬
{"EMail", "Email"}, ¬
{"Address", "Address"}, ¬
{"City", "City"}, ¬
{"State", "State"}, ¬
{"Zip", "Zip_Code"}, ¬
{"Country", "Country"}, ¬
{"Industry Type", "Industry_Type"}, ¬
{"Other Industry", "Other_Industry"}, ¬
{"Mailing List", "Mailing_List"}, ¬
{"Comments", "Comments"}}
set count_fields to count field_names
 
--import data
activate application "FileMaker Pro"
tell application "FileMaker Pro"
do script "New Record"
set this_record to create record
go to this_record
set matched_field to "false"
repeat with current from 1 to paragraph_number
set current_paragraph to paragraph current of new_message
set new_field to "false"
repeat with i from 1 to count_fields
if first text item of current_paragraph is item 2 of item i of field_names then
if matched_field is "true" then
set data cell this_field of this_record to field_data
end if
set this_field to item 1 of item i of field_names
set field_data to second text item of current_paragraph
set matched_field to "true"
set new_field to "true"
exit repeat
end if
end repeat
if new_field = "false" and matched_field = "true" then
set field_data to field_data & " " & current_paragraph
end if
end repeat
set data cell this_field of this_record to field_data
set data cell "Date Time" of this_record to (current date) as text
set data cell "New Record" of this_record to "yes"
if matched_field is "false" then
display dialog "Fatal error. The field " & this_field & " does not exist"
return 0
end if
end tell
set AppleScript's text item delimiters to old_delimiters
end import_data



Timing

How did I managed to spend upwards of 70 hours getting this little project to work, you may well ask? I think it may be helpful to the reader to get an idea of how much and in which way I blundered, so that you are fully prepared for what you are in for... and so you feel better when you blunder yourself. My time was spent as follows:

12 hours: searching for adequate documentation... You can't find Applescript books in New Hampshire, and I didn't want to order blindly over the Internet. I drove an hour to Barnes and Noble, who carried only Applescript for Dummies. But I didn't feel like I got an immediate overview of the language from this book. It seemed like the text was written as if that I didn't really want to learn Applescript, so I went looking for something else. I didn't even know until I had spent considerable time on the Web and had e-mailed several people that Apple provided an Applescript manual in pdf format that you can simply download off the Web. This also isn't mentioned in any Applescript books I have come across, of course, beecause it's not in their interest to let you know you can get documentation for free. And even after I finally ascertained that such a document existed, no one knew where this manual was actually located on the Web, nor did anyone I talked to at Apple technical support know. I spoke on 3 different occasions with 7 different people, and most of them had no idea even what I was talking about. I assumed this pdf manual to be fairly comprehensive, and it took me some time to realize that basic functionality, like 'choose file' were not even made mention of in this manual, because they are 'Scripting Additions' and not considered part of Applescript proper. So I had to read Applescript for Dummies as well.

5-6 hours: reading documentation

4 minutes: typing in the Applescript

8-10 hours: blundering around because I didn't conceive the project right... I didn't realize that Filemaker can send e-mail directly. The Filemaker manual only had one line on page 5-11, 'send mail', which if you included in an ordinary Filemaker script did nothing at all that I could see. There was no information whatever in the Filemaker manual that in order to get 'send mail' to work, you have to download and install shareware plug-ins or hook up to Internet Config. I surmised that 'send mail' did nothing, just like the 'Page Number' scripting command - which I had had previous experience with - does nothing, at least according to Filemaker technical support staff.

4-5 hours: Trying to figure out how to make various asynchronous events synchronous so that Applescript waited to read the mail until you were finished logging on, or waited to type in the custom reply before mailing it. All of this was also abandoned when I realized Filemaker could send e-mail.

2 hours: unsuccessfully trying to get the Eudora reply and delete commands to work. I still have no idea whether they work or how to implement them.

4-5 hours: Trying to get Eudora to open an e-mail through Applescript, a project which I finally abandoned when I realized Filemaker could send e-mail. The problem seems to be that Eudora only wants to open text files. The e-mail could just as well be a text file, so that was not so bad. But Applescript assumes that if you want to open a text file, even when you are telling Eudora, you want to use SimpleText, which isn't even scriptable. Since SimpleText isn't scriptable, I obviously couldn't use it to open the text file even had I wanted to. Every Macintosh file has assigned to it a file type, such as 'TEXT' or 'HTML' and a creator, which is supposed to indicate which application the file belongs to. Eudora, instead of assigning its own creator, assigns a generic creator to text files that it creates. In order to try to force Eudora to open the text file, I tried, for example, forcing the creator of the text file e-mails I wanted to open to be specifically Eudora's creator. I did this by using Macintosh Programmer's Workshops's (MPW -- Apple's own software development environment) 'set file' Shell script command with the -c option. This was surely above and beyond the call of duty. But that still didn't work. I tried 'choose file', of course. I tried simply 'open' inside and outside a tell Eudora statement. I tried a lot of things, and I still don't know how to get Eudora to open an e-mail through Applescript. If you are simply opening an e-mail in Eudora without trying to have Applescript do it for you, of course, it's trivial. The Applescript 'Open' command for Eudora therefore does not work like the Applescript 'Open' command for many other applications.

4-8 hours (probably): trying to understand Applescript type checking, classes and what not... how you coerce types and what types can be made into what others and when you have to coerce type. How to reference an element in a class in a general way. This is all not entirely trivial.

1 hour: trying to figure out how to write information into one cell on one record of one field in a Filemaker database. For example, if you do:

set data cell this_field of this_record to second text item of current_paragraph

it came out like I wanted. If you do:

set data cell this_field to second text item of current_paragraph

it doesn't warn you, but it does nothing at all. If you do:

set data field this_field to second text item of current_paragraph

it changes the data in that field to that value for all the records.

15 hours: trying to figure out how best to access text. Most of these commands that were supposed to work like 'third word of second paragraph' simply didn't work. I finally was tipped off by a hacker that I should set the Applescript delimiters to other values than the default, and then talk to Applescript in terms of 'first text item' and 'second text item'.

2-3 hours: setting up and beautifying the Filemaker database, writing the scripts, making pretty headers, etc..

1 hour: trying to figure out how to call a subroutine from within a tell command. This is the disadvantage of feeling so rushed you don't just read all the documentation carefully cover to cover. All manuals tell you about the my command, of course. But when the call to a subroutine suddenly doesn't work, you don't know where to look. No sign comes up saying '"he problem is the 'tell' command. Use my." Your first thought may be that maybe you've spelled something wrong. Maybe you've sent the wrong parameters to the subroutine... their type may be wrong. You can spend ages looking in all the wrong places. But if you suspect what the problem is, you can look it up in 4 minutes or less. You don't lose time looking it up. You lose time guessing what to look up. I had never programmed in a language that had anything like a 'my' command, so I had no idea that I was even missing anything.

1 hour: unsuccessfully trying to get Filemaker to actually run the Applescript it called, and not just bring Applescript up. (I later found out, of course, that this is utterly trivial.)

Does that make 70 hours? I must have spent the rest of the time listening to a Roland Hayes tape and drinking hot chocolate. And no, I didn't charge the poor client for all those hours I spent clueless... Anyway, the moral of the story from my perspective is this: Read all the documentation first... It won't take 70 hours. And also make sure you know the applications you're working with. And be prepared to stumble more than you would with other programming languages, because there are many more things that can go wrong, many more cooks for this soup.


And then of course, just as I finished, ACME came out with a plug-in for Filemaker that supposedly both receives and sends e-mail much faster and better than anything one can write in Applescript can, because Applescript, being interpreted rather than compiled, is quite slow. The client later found that the ACME plug-in didn't work right, so they're back to using Applescript.


The information provided here will get you pretty far. Applescript has other functionality not covered here. The only major functions that haven't been mentioned at all are (I think) properties and script objects (instances of script objects and inheritance).

Properties - are like global variables that, that retain their value from one execution of the script to the next. For example, if you write a script to back up files, you need to remember from one day to the next which files existed the last time you backed up and what the modification dates were. You store that type of information in properties. These values are, however, reset when you compile a script and use the check_syntax button!

Script Objects - This is are basic object-oriented functionality which you will need for more involved applications. You define a 'script object' with (predictably) script and end script. Then you make a copy of that script object, which in the jargon is called an 'instance' of a 'class'. The class defines a template with certain parameters unspecified, and when you make an instance of that class, you fill in the blanks... The class is like an application form which is blank, and when you create an instance of the class, it's analogous to filling out the form.

Typically, what you do is this. You create a handler (subroutine) using the on command. The first thing inside the handler is typically a bunch of property statements which define the information you are really interested in getting, be it a username or a list of folders or whatever. Following the properties list is typically a script command which defines a script object. Between the script and end script, you find a string of handlers (sub-routines) defined by on statements which define the functionality of the script. If the script object is for parsing an incoming e-mail, it might have handlers that get the e-mail address, reformat the body of the text... who knows what you may need to do with the e-mail. One of these handlers (typically the first) is called init, and it initializes the script object typically by setting the properties using the other handlers inside the script. Finally, after the end of the script object, but before ending the handler, you call that init subroutine. You call script object commands inside a tell statement as if it were an application.

Example:

on make_object (thing_to_process)
script object
property info1: ""
property info2: ""
on init(thing_to_process)
set info1 to subroutine1 (thing_to_process)
set info2 to subroutine2 (info1)
end init
on subroutine1 (object)
-calculate what you need to here
return result_of_process1
end subroutine1
on subroutine2 (pick_a_name)
-calculate what you need to here
return result_of_process2
end subroutine2
end script
tell object
init (thing_to_process)
end tell
return object
end make_object

After that you need to call the main handler make_object and set the result to some variable. Then you use that variable to get at all the other information in the script object. Like this:

set the_object to make_object("specific_thing_to_process")
set info_type1 to info1 of the_object
set info_type2 to info2 of the_object
return (the_object, info_type1, info_type2)

Now info_type1 and info_type2 will have the specific information that this particular script object produces for that thing. If subroutine1 returns the e-mail address of some e-mail, that gets placed in info1 by the init, and info_type1 will hold the e-mail address of the specific e-mail called "specific_thing_to_process". Similarly info2 can be anything... the body of the e-mail reversed, bolded and translated into Spanish... or the file path to a picture of your grandmother.

Finally, you have the concept of 'inheritance' in object-oriented programming. This is a way to simplify and save a lot of coding in bigger applications. Imagine once again that you are importing e-mails. You need to get the 'sender' from every e-mail regardless, but depending on the sender, you process the e-mails in different ways. Some e-mails you store in a specific folder. Others you parse and enter into a Filemaker database, and so on. Since you have to get the sender regardless, you can define a script object at the highest level that only gets the sender. That's called the parent. Then you define more specific script objects which have the functionality of the parent and then some. One child of the basic get-email script object that only checks for the sender will have an additional handler to save the e-mail to disk and another will parse the e-mail and import it into Filemaker. Languages like JAVA use this rampantly. One parent routine draws a circle of a specific size and a specific location on the screen. Its children use the parent to draw the circle and then they color it, fill it in, etc.. You define a parent like this:

script parent_script
property parent_info1: ""
on function1 (object)
-calculate here
end function1
on function2 (object)
-calculate here
end function2
end script
 
on make_object (thing_to_process)
script child_script
property parent: parent_script
property child_info1: ""
on init (thing_to_process)
set my parent_info1 to function1 (thing_to_process)
set child_info1 to function2 (thing_to_process)
end init
end script
tell child_script
init (thing_to_process)
end tell
return object
end make_object

set the_object to make_object("specific_thing_to_process")
set info_type1 to parent_info1 of the_object
set info_type2 to child_info1 of the_object
return (the_object, info_type1, info_type2)

There is also the capability within the child object to override the parent's handlers. You do this using the continue statement from within the child's overriding handler thusly:

script child
-...
on parent_handler1 (parameter1, parameter2)
-extra functionality here
continue parent_handler1 (parameter1, parameter2)
end parent_handler1
-...
end script



My Web Site
Margo's Magical Letter Page