19.2. LoopingThe 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 , 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:
19.2.1. Repeat ForeverA 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 TimesA 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 WhileA 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 UntilA 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
19.2.5. Repeat WithThe 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:
If you read the description carefully, you will realize that:
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... InThe 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:
(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 |