[ Team LiB ] |
12.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, 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:
12.2.1 Repeat ForeverA 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 WhileA 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 UntilA 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
12.2.4 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!" 12.2.5 Repeat With...InThe 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 TimesA 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.
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 LoopsYou 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 ] |