Previous Page
Next Page

19.2. Looping

The other major form of choice is looping , which involves branching back to the start of a block repeatedly. In AppleScript, looping is performed with repeat. There are several varieties of repeat, but repeat blocks all take same basic form:

repeat whatKindOfRepeat
    -- what to do
end repeat

Loops involve repetitionperhaps a lot of repetition. Therefore, although I'm no great believer in worrying too much about optimization, if you're going to optimize your code anywhere, loops are the place to do it. A small increase in speed can add up tremendously over multiple repetitions. See Chapter 22.


The big question with a repeat block is how you're going to get out of it. Obviously you don't want to repeat the repeat block forever , because that would be an infinite loop and would cause the computer to hang. Most kinds of repeat block include some instruction (as symbolized by whatKindOfRepeat in the syntax template), such as a condition to be evaluated, as a way of deciding whether to loop again.

There are also some special commands for hustling things along by leaping completely out of the repeat block from inside it. These are the premature terminations of a repeat block. They can be used with any form of repeat block. Here they are:


exit repeat

This statement exits the innermost repeat block in which it occurs. Execution resumes after the end repeat line.


try block

If the repeat block is inside a try block, throwing an error exits the repeat block by virtue of the fact that it exits the try block. See "Errors," later in this chapter.


return

A return statement exits the repeat block by virtue of the fact that it terminates execution of the handler or script.

19.2.1. Repeat Forever

A repeat block with no whatKindOfRepeat condition repeats forever. Obviously you don't really want it to repeat forever, so it's up to you to supply a way out by using one of the three premature terminations.

repeat
    display dialog "Prepare to loop forever."
    exit repeat
end repeat
display dialog "Just kidding."

19.2.2. Repeat N Times

A repeat block where whatKindOfRepeat is an integer followed by the keyword times repeats that number of times (unless the block terminates prematurely). The integer can be a variable.

repeat 3 times
    display dialog "This is really boring."
end repeat
display dialog "ZZzzzz...."

An interesting misuse of this construct is as a workaround for AppleScript's lack of a next repeat or continue keyword. (This idea was suggested to me by Paul Berkowitz, who attributes it to Ray Robertson.) The problem is that you can short-circuit a repeat block by exiting it completely, but you cannot, as you can in many languages, short-circuit it by proceeding immediately to the next iteration of the loop. The workaround is to embed a one-time repeat block within your repeat block; an exit repeat within this one-time repeat block works as a next repeat with respect to the outer repeat block. This device has some shortcomings (it prevents exit repeat from doing its proper job), and it doesn't accomplish anything you couldn't manage with an if block, but in a large script it can make your code easier to read and maintain.

In this (silly) example, we add only the positive items of a list:

set L to {2, -5, 33, 4, -7, 8}
set total to 0
repeat (count L) times
    repeat 1 times
        set x to item 1 of L
        set L to rest of L
        if x < 0 then exit repeat
        set total to total + x
    end repeat
end repeat
total -- 47

For another useful misuse of this construct, see "Blocks" in Chapter 5.

19.2.3. Repeat While

A repeat block where whatKindOfRepeat is the keyword while followed by a boolean condition tests the condition before each repetition. If the condition is true, the block is executed. If the condition is false, the block is not executed and that's the end of the loop; execution resumes after the end repeat line. The idea is that in the course of looping something will eventually happen that will make the condition false.

set response to "Who's there?"
repeat while 
 response = "Who's there?"
    set response to button returned of 
(display dialog "Knock knock!" buttons {"Enough!", "Who's there?"}) end repeat

19.2.4. Repeat Until

A repeat block where whatKindOfRepeat is the keyword until followed by a boolean condition tests the condition before each repetition. If the condition is false, the block is executed. If the condition is true, the block is not executed and that's the end of the loop; execution resumes after the end repeat line. This construct is technically unnecessary, as the very same thing could have been achieved by reversing the truth value of the condition of a repeat while blockthat is to say, repeat until is exactly the same as repeat while not.

set response to ""
repeat until response = "Enough!"
    set response to button returned of 
(display dialog "Knock knock!" buttons {"Enough!", "Who's there?"}) end repeat

Those accustomed to the do...until construct in C and similar languages should observe that it is possible for an AppleScript repeat until block not to be executed even once.


19.2.5. Repeat With

The syntax of a repeat with announcement line is as follows:

repeat with variableName from startInteger to endInteger [by stepInteger]

Here's how a repeat with works:

  1. When the repeat with announcement line is encountered for the first time, startInteger and endInteger (and stepInteger, if supplied) are evaluated once and for all. Each must be an integer (or a real or string representing an integer); if not, there's a runtime error.

  2. If startInteger is larger than endInteger (or smaller if stepInteger is negative), that's the end of the loop and execution resumes after the end repeat line.

  3. The value startInteger is assigned to the variable variableName, which is created as a local if not in scope already. (Note that variableName, if declared implicitly at the top level, will not be a global; this contradicts "Scope of Undeclared Variables" in Chapter 10.)

  4. The block is executed. (If code within the block terminates the repetition prematurely, that's the end.)

  5. The value 1 (or stepInteger if supplied) is added to the value that variableName was assigned before the previous repetition. If the resulting value is larger than endInteger (or smaller if stepInteger is negative), that's the end of the loop and execution resumes after the end repeat line. Otherwise, variableName is assigned this new value, and it's back to step 4 for another execution of the block.

If you read the description carefully, you will realize that:

  • There's no extra overhead involved if any of the integers in the repeat with line are derived from handler calls or commands, as the evaluation is performed only once. This behavior is in contrast to repeat while and repeat until.

  • After a repeat with is all over, the variable variableName has the value it had when the last repetition terminated.

  • Setting the variable variableName within a repeat with block affects the code that executes subsequently within the block, but it has no effect on the test performed at the top of the next repetition or on what value variableName will take on as the next repetition begins.

Here's a simple example of repeat with in action:

repeat with x from 3 to 1 by -1
    display dialog x
end repeat
display dialog "Blast off!"

19.2.6. Repeat With... In

The syntax of a repeat with...in announcement line is as follows:

repeat with variableName in list

This construct is a convenient way of cycling through each item of a list. Here's how to visualize the way it works:

  1. The variable variableName is created as a local if it is not already in scope.

  2. The size of the list is obtained. Call this number theCount.

  3. A reference to the list is obtained. Call this theListRef.

  4. A counter is initialized to zero. Call it x.

  5. Before each repetition, x is incremented. If x exceeds theCount, that's the end of the loop, and execution resumes after the end repeat line. Otherwise, variableName is assigned this value:

    a reference to item x of theListRef

  6. The block is executed.

(In this sequence of steps, none of the variable names are real: there is nothing called theCount, theListRef, or x. But AppleScript is truly maintaining these values internally; I gave them names simply for clarity.)

The big surprise here is step 5, which sets the value of variableName. At the start of each repetition, nothing is copied from the list into variableName. It's a reference (see Chapter 12). So, for example, in this loop:

repeat with x in {1, 2, 3}
    -- code
end repeat

the variable x does not take on the values 1, 2, 3. It takes on these successive values:

a reference to item 1 of {1, 2, 3}
a reference to item 2 of {1, 2, 3}
a reference to item 3 of {1, 2, 3}

References are often transparently dereferenced, so the fact that variableName is a reference might not make a difference to your code. For example:

repeat with x in {1, 2, 3}
    display dialog x -- 1, 2, 3
end repeat

In that example, the reference is implicitly dereferenced, and the value of each item is retrieved from the list. But consider what happens when the reference is not implicitly dereferenced. For instance, you might apply to variableName the equality or inequality operator:

repeat with x in {1, 2, 3}
    if x = 2 then
        display dialog "2"
    end if
end repeat

The dialog never appears! That's because x is never 2. The second time through the loop, x is a reference to item 2 of {1, 2, 3}; that's not the same thing as the integer 2, and AppleScript doesn't implicitly dereference the reference. Clearly your script will misbehave if you're unprepared. The solution is to dereference explicitly:

repeat with x in {1, 2, 3}
    if contents of x = 2 then
        display dialog "2"
    end if
end repeat

Here's another example; we'll retrieve each value and store it somewhere else:

set L1 to {1, 2, 3}
set L2 to {}
repeat with x in L1
    set end of L2 to x
end repeat

What do you think L2 is after that? If you said {1, 2, 3}, you're wrong; it's this:

L2 -- {item 1 of {1, 2, 3}, item 2 of {1, 2, 3}, item 3 of {1, 2, 3}}

L1 is a list of values; L2 is a list of references. If you want L2 to end up identical to L1, you must dereference each reference:

set L1 to {1, 2, 3}
set L2 to {}
repeat with x in L1
    set end of L2 to contents of x
end repeat
L2 -- {1, 2, 3}

A powerful consequence of the fact that variableName is a reference to an item of a list is that you can use it to assign back into the original list:

set L to {1, 2, 3}
repeat with x in L
    set contents of x to item x of {"Mannie", "Moe", "Jack"}
end repeat
L -- {"Mannie", "Moe", "Jack"}

We can take advantage of this technique to rewrite the return-by-reference example from the end of Chapter 12 in a different way:

on findInList(what, L)
    repeat with anItem in L
        if contents of anItem is what then
            return anItem
        end if
    end repeat
    return
end findInList
local pep
set pep to {"Mannie", "Moe", "Jack"}
set contents of findInList("Moe", pep) to "Larry"
pep -- {"Mannie", "Larry", "Jack"}

On the whole, you should probably not make more radical alterations to the list during the course of a repetition, but it is not unsafe to do so. If you assign a completely new value to the list variable, there is no change in the behavior of the loop, because a reference to the old list was already captured at the outset (that is the significance of the listRef variable in my explanation of how this construct works). So:

set L to {1, 2, 3}
repeat with x in L
    set L to {"Mannie", "Moe", "Jack"}
    display dialog x -- 1, then 2, then 3
end repeat

Why does that work? It's because on each repetition, x takes on these values, just as before:

a reference to item 1 of {1, 2, 3}
a reference to item 2 of {1, 2, 3}
a reference to item 3 of {1, 2, 3}

You changed what L points to, but listRef already holds a reference to the original list. On the other hand, if you mutate the list in place, listRef is still a reference to that same list, so it acquires the mutations:

set L to {1, 2, 3}
repeat with x in L
    display dialog x -- 1 (every time)
    set beginning of L to contents of x
end repeat
L -- {1, 1, 1, 1, 2, 3}

The dialog says "1" every time because, for example, by the time we come to the third loop and get a reference to item 3 of L, item 3 of L is 1. Observe that this code did not cause an infinite loop. That is the significance of step 2 in my explanation of how this construct works: theCount was evaluated once, before the first repetition, and you won't repeat more times than that.

A very odd thing happens when you combine repeat with...in directly with a class name as a way of gathering a list of elements, like this:

repeat with x in every class

To see what I mean, consider this code:

set total to 0
tell application "Finder"
    count folders -- 6
    repeat with x in every folder
        set total to total + 1
    end repeat
end tell
total -- 50

I've got only six folders on my desktop, so where did the number 50 come from? To put it another way, what did we just cycle through? The answer is that we didn't cycle through each folder; we cycled through each item inside each folder. (So 50 is the total number of files and folders inside the six folders on my desktop.) Here's why. When you talk like this, AppleScript does not ask the target application for a list of all the things you specify. Instead, it merely asks the target application for a count of those things. And it asks for this count in a very strange form:

count every class each item

Most applications simply shrug off this extra "each item"; most of their classes don't have an element called item, so they ignore this part of the command. For example, when you say this:

tell application "Microsoft Entourage"
    repeat with x in every contact

AppleScript asks Entourage to "count every contact each item." Entourage contacts don't have an item element, so Entourage treats this as if it were "count every contact" and simply reports the number of contacts. But in the Finder, a folder does have items, so the Finder obligingly totals up the number of items within each folder and reports that total.

This is not, however, simply an odd behavior on the part of the Finder; my point is what AppleScript does. We've just seen that when you say this:

repeat with x in every class

AppleScript does not gather a list. It merely obtains a number. So how on earth does it perform the loop? In other words, what is x every time through the loop? When you say this:

tell application "Microsoft Entourage"
    repeat with x in every contact

then on successive passes through the loop the value of x is this:

a reference to item 1 of every contact
a reference to item 2 of every contact
a reference to item 3 of every contact

And so on. AppleScript uses the class you asked for to form a reference to a particular item of the list that would be generated if we actually asked for it. But it doesn't ask for that list unless you try to fetch that item. This looks at first blush like an optimization, but if you think about, it's really quite the opposite. Consider what happens when you run this script:

tell application "Microsoft Entourage"
    repeat with x in every contact
        get name of x
    end repeat
end tell

That is already a wasteful script, as you could have obtained the names of all contacts with a single Apple event by asking for "name of every contact." But it's even more wasteful than you might suppose. Each time through the loop, you are not asking for "name of contact 1," "name of contact 2," and so forth. You are asking for "name of item 1 of every contact," "name of item 2 of every contact," and so forth. In other words, you are asking Entourage to form a list of all contacts, every time through the loop! That's unnecessary, because the list doesn't change.

This situation gets even worse if the list you're asking for is to be generated in a more complicated way. Here is some code where we try to get BBEdit to change all words that start with "t" to the word "howdy":

 tell application "BBEdit"
    repeat with w in (every word of document 1 where its text begins with "t")
        set text of w to "howdy"
    end repeat
end tell

There are two problems with this code. First, it's incredibly wasteful, because every time through the loop, w is something like this:

a reference to item x of every word of window 1 of application "BBEdit" 
where its text begins with "t"

Thus, if we do anything with w, we are asking BBEdit to perform this complicated boolean test (every word of window 1 where its text begins with "t") every time through the loop!

The second problem is that the code doesn't work. If there are two words starting with "t" in the document, it fails with a runtime error. The reason is that the second time through the loop, we are asking for this:

a reference to item 2 of every word of window 1 of application "BBEdit" 
where its text begins with "t"

But there is no such item, because there were originally two such words and we just changed one of them to "howdy," so now there is only one such word.

The solution to this entire issue is: Don't Do That. Simply don't talk like this:

repeat with x in every class

AppleScript's response to this way of talking may have been intended as an optimization, but it's just a mess. The simple solution is to gather the list explicitly, in the same line or an earlier line. So, say this:

repeat with x in (get every class)

Or this:

set L to (get every class)
repeat with x in L

All the problems with repeat with...in vanish in a puff of smoke if you do this. You ask for the list just once, at the start of the loop. That's the only time the application has to form the list, so execution is far less wasteful. You actually obtain the list you ask for, so you're looping through what you think you're looping through. So, for instance, this works properly:

tell application "Finder"
    repeat with x in (get every folder) -- loops 6 times, as expected

And here's a quick, efficient, and correctly working version of our BBEdit script:

tell application "BBEdit"
    set L to (get every word of document 1 where its text begins with "t")
    repeat with w in (reverse of L)
        set text of w to "howdy"
    end repeat
end tell


Previous Page
Next Page