DekGenius.com
[ Team LiB ] Previous Section Next Section

12.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

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, since this will be an infinite loop and will cause the computer to hang. Usually you deal with this through the nature of the whatKindOfRepeat, which typically provides a condition to be evaluated, as a way of deciding whether to loop again, or some other form of instruction governing how many times the block will be repeated.

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


return

This command leaves the repeat block by virtue of the fact that it terminates execution of the handler or script.


exit repeat

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

12.2.1 Repeat Forever

A repeat block with no whatKindOfRepeat clause repeats unconditionally, whence forever. Obviously you don't really want it to repeat forever, so it's up to you to supply a way out. I just told you two ways out. A third is to loop inside a try block and to throw an error; this method is illustrated later in this chapter, in Section 12.7.2.

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

12.2.2 Repeat While

A repeat block where whatKindOfRepeat is the keyword while followed by a boolean expression tests the expression before each repetition. If the expression is true, the block is executed. If the expression 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 expression 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

12.2.3 Repeat Until

A repeat block where whatKindOfRepeat is the keyword until followed by a boolean expression tests the expression before each repetition. If the expression is false, the block is executed. If the expression 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, since the very same thing could have been achieved by reversing the truth value of the condition of a repeat while block—that 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 other, C-like languages should observe that it is possible for an AppleScript repeat until block not to be executed even once.


12.2.4 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 line is encountered for the first time, startInteger and endInteger (and stepInteger, if supplied) are evaluated once and for all, and coerced to integers if possible. (If it isn't possible, 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.

This fact is an exception to the rule that implicitly declared variables at top level are global (Section 7.4.5).


  1. The block is executed.

  2. The value 1 (or stepInteger if supplied) is added to the value that variableName was assigned at the start of 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 the block is executed and this step repeats.

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, since the evaluation is performed only once. This 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!"

12.2.5 Repeat With...In

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

repeat with variableName in list

This construct is much like a repeat with, but the variable variableName is assigned successively a reference to each item of the list.

When I say a reference, I mean it (see Chapter 11); nothing is copied from the list into variableName. So, for example, in this loop:

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

the variable x takes on these successive values:

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

In some contexts, the fact that variableName is a reference won't make a difference to your code, because references are transparently dereferenced much of the time. For example:

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

Here, the reference is implicitly dereferenced, and the value of each item is retrieved from the list. But things are different, for example, when you use 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 the reference item 2 of {1, 2, 3}; that's not the same thing as the integer 2, and AppleScript doesn't implicitly dereference the reference. The solution is to dereference it 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}}

L2 is not the same as L1. 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}

When, as here, variableName is a reference to an item of a list, 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"}

A loop can alter the value of an item of the list before encountering it, and it can increase the size of the list. Thus:

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

Observe that this did not cause an infinite loop. AppleScript reads the size of the list once, before the first repetition; you won't repeat more times than that.

It can be important to be cognizant of the details of the references in the list. Recall the example from Section 1.3 where we renamed the files in a folder, first gathering their names like this:

set allNames to name of every item of theFolder
repeat with aName in allNames

Why didn't we cycle through the items of the folder directly, like this?

repeat with anItem in theFolder

In the second formulation, the references in the list are to item 1 of folder..., item 2 of folder..., and so forth. But we're going to be changing the names of items in this folder while we're cycling, and changing the name of an item may alter the way the items are numbered. Thus we might not cycle through each item of the folder after all. Remember, a reference is a frozen expression, not a magic pointer (Section 11.1.2).

We now turn to considerations of efficiency. Here is some code that makes BBEdit capitalize every word that starts with "t":

tell application "BBEdit"
        repeat with w in every word of window 1
                if contents of text of w begins with "t" then
                        change case w making raise case
                end if
        end repeat
end tell

It works, but we are sending at least twice as many Apple events as there are words in the document. Let's try to shorten the list to just the words that start with "t":

tell application "BBEdit"
        repeat with w in (every word of window 1 ¬
                where contents of text of it begins with "t")
                change case w making raise case
        end repeat
end tell

This fails with a runtime error. To see why, we investigate the successive values of w, and we learn something very interesting. It turns out that during the first repetition, w is a reference to this:

item 1 of every word of window 1 of application "BBEdit" ¬
        whose contents of every text starts with "t"

We capitalize that word, and now we proceed to the next item, which is a reference to this:

item 2 of every word of window 1 of application "BBEdit" ¬
        whose contents of every text starts with "t"

This explains the runtime error. For example, if at the outset there were just two words beginning with lowercase "t", there is no such item as this, because we just capitalized one of those words, so there's now only one word in the window beginning with lowercase "t"! As we can see, w is being set to these curious references of the form item 2 of every word.... The expression every word...whose is thus being evaluated afresh every time through the loop. This, in addition to breaking our code, is a further source of inefficiency: we're making BBEdit perform this entire complicated boolean test each time through the loop, when it should suffice to perform it once.

The cause is a very odd feature of how AppleScript behaves when you say something like this:

tell some application
        repeat with x in every ...

When AppleScript sees this form of repeat with announcement line, it responds by sending an Apple event to the target application. You might expect that this would be a request for a list of references; but it isn't. Instead, AppleScript merely asks the target application how many such references there are; it sends a count command, not a get command. Presumably AppleScript imagines it will be a lot more efficient to ask for one little number than for a list of who knows how many references.

The solution is to use get yourself:

tell application "BBEdit"
        repeat with w in (get every word of window 1 ¬
                where contents of text of it begins with "t")
                change case w making raise case
        end repeat
end tell

That works fine, because w is now set to values like this:

item 1 of {characters 11 thru 12 of text window 1 of application "BBEdit"}

And it's a lot more efficient too.

The implication seems to be that you should probably use get in constructs of this kind. In fact, if you don't use get, many applications won't be able to respond at all to what you say in the loop. The Finder is a good example. Suppose we want to gather the names of all folders. This doesn't run at all:

set L to {}
tell application "Finder"
        repeat with f in every folder
                set end of L to (get name of f) -- error
        end repeat
end tell

This works fine:

set L to {}
tell application "Finder"
        repeat with f in (get every folder)
                set end of L to (get name of f)
        end repeat
end tell

The reason is that in the first form when you say get name of f, you're saying get name of item 1 of every folder and so on, and the Finder interprets this to mean, not the first item of a list of folders, but a list of the first items on disk inside each folder! In the second form, you start by gathering references to each individual folder; then you use each reference to ask for the name of that folder.

12.2.6 Repeat N Times

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

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

An interesting use of this construct is to implement a workaround for AppleScript's lack of a next repeat keyword.[1] The problem is that you can short-circuit a repeat block by exiting it completely, but you cannot, as in many languages, short-circuit it by proceeding immediately to the next iteration. 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 doesn't accomplish anything you couldn't manage with an if block, but it can prove more legible and maintainable.

[1] This idea is suggested to me by Paul Berkowitz, who attributes it to Ray Robertson.

For example:

set L to {"Mannie", "Moe", "Jack"}
set L2 to {}
repeat with aBoy in L
        repeat 1 times
                if aBoy does not start with "j" then exit repeat
                set end of L2 to contents of aBoy
        end repeat
end repeat
L2 -- {"Jack"}

For another useful misuse of this construct, see Section 6.5.

12.2.7 Being Careful with Loops

You probably think I'm about to say, "Watch out for infinite loops." You're wrong. Quite frankly, I don't care if your loops go on till Doomsday. But I do care if they do more work than they have to.

Keep in mind that Apple events are expensive, and some Apple events are very expensive. While I'm not a great believer in worrying about code optimization, you should probably take a little elementary care with your loops to see that your Apple events are as few and as simple as possible.

Observe, for instance, that the boolean expression at the top of a repeat while block must be evaluated before every repetition of the block, and then once more in order to decide not to repeat the block any further. This means that it should not contain any commands whose result will not change during the repetition, since this would be needless and wasteful overhead.

For example, it would be foolish to write this:

set x to 1
tell application "Finder"
        set f1 to folder "f1"
        set f2 to folder "f2"
        repeat while ((count items of f1) < (count items of f2))
                make new folder at f1 with properties {name:("f" & x)}
                set x to x + 1
        end repeat
end tell

That code sends the count message to the Finder six times when in fact we need only send it twice:

set x to 1
tell application "Finder"
        set f1 to folder "f1"
        set f2 to folder "f2"
        set c1 to count items of f1
        set c2 to count items of f2
        repeat while c1 < c2
                make new folder at f1 with properties {name:("f" & x)}
                set x to x + 1
                set c1 to c1 + 1
        end repeat
end tell

The example itself is rather a silly way to perform this task, but the lesson it illustrates is very real.

Our earlier example using BBEdit exposes the same issue. At first, you remember, we said this:

tell application "BBEdit"
        repeat with w in every word of window 1
                if contents of text of w begins with "t" then
                        change case w making raise case
                end if
        end repeat
end tell

This sets w each time to a reference of this form:

item 1 of every word of window 1 of application "BBEdit"

That code sends BBEdit two Apple events for every word in the window, one of which asks BBEdit to evaluate the concept every word afresh each time! If there are a thousand words and just two beginning with "t", that's a massive waste. The second version of our code was much better:

tell application "BBEdit"
        repeat with w in (get every word of window 1 ¬
                where contents of text of it begins with "t")
                change case w making raise case
        end repeat
end tell

If there are just two words beginning with "t", that code will send just three Apple events: one to gather the list of references to the two words, and then two more to change their case.

Also, be alert for the possibility that you might not have to loop at all. The target application might be smart enough do what you want with a single command. In the earlier Finder example, there was actually no need to gather a reference to every folder and then ask for the name of each; if that's all we wanted, it could have been done like this:

tell application "Finder" to set L to name of every folder

See Section 10.8.

    [ Team LiB ] Previous Section Next Section