TOP | UP: UserTalk Basics | NEXT: Running and Debugging Scripts |
This chapter describes the UserTalk control structures that have not already been described. They are categorized as looping constructs, visit constructs, conditional constructs, and errors.
For on and return, see Chapter 5, Handlers and Parameters. For with, see Chapter 6, Referring to Database Entries. For local, see Chapter 7, The Scope of Variables and Handlers. For bundle, see Chapter 4, What a UserTalk Script Is Like, and Chapter 7.
Control structures all involve indented bundles, as explained in Chapter 4; for the significance of this with respect to scope, see Chapter 7.
A looping construct provides a way to perform the same commands over and over. Repetition is the essence of computer programming; most programming tasks are solved by finding a way to express them as a loop. From a theoretical point of view, all looping constructs are equivalent; a variety is provided merely for convenience of expression.
Syntactically, the pattern is that the construct is introduced by a keyword, usually with further information; the commands to be repeated then appear as a bundle indented from the keyword's line. For example:
for x = 1 to 10
msg(x)
clock.waitseconds(1)
msg("All done!")
The repeated task here is to display the value of x in the Main Window and then wait for one second. The nature of the repetition is dictated in the for line: here, the task is to be performed ten times, with x taking on a new value each time - first 1, then 2, and so on until it has been performed with x being 10. Only then will execution proceed to the next command, which displays All done! in the Main Window.
Two keywords provide ways of short-circuiting a loop. The continue statement skips all subsequent commands within the loop and proceeds to the next repetition (if any). For example:
for x = 1 to 10
msg(x)
if x > 5
continue
clock.waitseconds(1)
msg("All done!")
All ten iterations are performed, but in the last five, where x is more than 5, the clock.waitseconds() line is skipped. So the routine counts from 1 to 5 slowly and then from 6 to 10 very quickly, and then says All done!.
The break statement jumps out of the loop altogether. Thus:
for x = 1 to 10
msg(x)
if x > 5
break
clock.waitseconds(1)
msg("All done!")
counts from 1 to 6 slowly before the if test succeeds. Then All done! is displayed.
The continue and break statements are as close as UserTalk comes to having a goto . They apply only to loops; it is not an error to execute a continue or break outside of any loop, but the result is that execution is completely aborted. They apply only to the innermost loop containing them; they cannot leap out to a higher level. Use of local handlers and try...else (discussed later in this chapter) can provide that sort of control.
for x = num1 to num2
for x = num1 downto num2
Any variable or database entry name can be used for the iteration variable on the left side of the assignment. Both num1 and num2 must be integers (positive, negative, or zero); if not, they will be coerced to integers (if possible).
Before any action is taken, num1 and num2 are compared. If num1 is less than or equal to num2 (in the first form), or greater than or equal to num2 (in the second form), the iteration variable is assigned the value of num1 and the indented bundle is performed. Subsequently, num1 is maintained in Frontier's internal memory: its value is incremented by 1 (in the first form) or decremented by 1 (in the second form) and compared to num2 in the same way as before, and if the test succeeds, then the iteration variable is assigned that new value and the indented bundle is performed again - and so on, until the comparison fails, whereupon the value of the iteration variable is left untouched and the indented bundle is skipped, and we go on with the script.
Thus it is perfectly possible that the loop will never be performed, and that the iteration variable will never be assigned a value by the for line. This suggests a useful trick for ascertaining whether the loop was ever performed: give the iteration variable some telltale value beforehand, and test for it afterwards:
local (x = "never")
for x = value1 to value2
« whatever
if x == "never"
« then we know the loop was never performed
It is perfectly legal, within the loop, to make changes to the value of the iteration variable. This does no harm to the iteration process, because it is not really the iteration variable that is incremented or decremented before each repetition: it is the most recent value of num1 in Frontier's internal memory. This value will be assigned to the iteration variable if the loop is going to be performed again, overriding any change to the iteration variable made within the loop.
Since, after the last iteration, the iteration variable's value is left untouched, it can be used after the loop. This is useful, among other things, for finding out how many iterations were actually performed.
Here, for example, is a utility which, given an array, reports the number of its first item having a given value. This is not the cleverest way to write such a utility, but it illustrates the use of the iteration variable after the loop is over.
for x in myList
where myList must be a record, a list, or something coercible to a list. Again, any variable or database entry name can be used for x. The construct loops through the indented commands, assigning to x before each iteration the value of each item of myList, in order. The result is exactly like saying:
for i = 1 to the sizeof(myList)
x = myList[i]
« ...
The shorthand construct is very useful, but ordinary for is sometimes necessary (as in Example 12-1).
The keyword is followed by an expression which evaluates to (or can be coerced to) a boolean. If the boolean is true, the indented bundle is performed; this process is repeated until the boolean is false, at which point the indented bundle is skipped and execution proceeds. Note that the boolean is evaluated anew before each repetition, and that any variables referred to within it may have new values as a result of the previous repetition; this is in contrast to for, where num1 and num2 are evaluated only once and the iteration variable is set by Frontier before each iteration.
A typical use is to initialize some boolean or counting variable beforehand, check it in the while line, and change it during the loop. Recall these lines from Example 4-7:
local (t = op.level ())
while --t {s = s + " "}
The idea here is to reflect, using spaces, the depth of the current line of a script or outline. We use the pre-decrement operator (see Chapter 15, Math) to take care of the subtraction for us right in the while line; we can use a number as a boolean because every number except 0 is coerced to true.
This construct has two syntaxes. In the first, there are no parentheses, nothing follows the word loop on its line, and we just loop forever; it is up to the commands inside the loop to escape, typically with break or return. The same thing can be accomplished by saying while true:
local (x = 1)
loop « could have said, while true
msg(x++)
if x > 10 {break}
In the second syntax, the parentheses contain three items, separated by semicolons. The first item is a command to be performed before the first iteration of the loop; the second item is a boolean expression to be evaluated before each iteration (the indented bundle will be executed only if this is true); the third item is a command to be performed before each iteration except the first. This will be clearer if we call the three items p1, p2, and p3 ; then, on the first pass, we perform p1 and check p2; on subsequent passes we perform p3 and check p2 ; if p2 is false we skip the indented bundle and go on with the script.
There is nothing here that could not be done with a while loop; but the syntax is very convenient. Suppose, for example, we want to do what a for loop does but we want to increment by our iteration variable by .1 instead of 1. A for loop doesn't allow this. So we might use a loop() construct instead, saying, for example:
loop (local (x = 1.0); x <= 2.5; x = x + 0.1)
« ...
As with while, any variable used in the loop() line can be altered in the indented commands, and it is the altered value that will be used as preparations are made to perform the loop again. For example, what does this script do?
loop (local (x = 1.0); x < 2.5; x = x + 0.1)
msg(x)
clock.waitseconds(1)
x = x + 4
msg(x)
This construct is for looping over files in a folder on disk. The basic syntax is:
fileloop (f in folderPathname)
The name f is just by way of example; it can be any variable or database entry name. On each iteration of the loop, f is assigned the full pathname of a different file or folder in the folder designated by folderPathname. The indented bundle uses this information as desired. For example, here is a script that lets the user select a folder and then reveals whether that folder contains any invisible files or folders:
local (f, whatFolder, foundAny = false, s)
if not file.getFolderDialog ("Please choose a folder.", @whatFolder)
return
fileloop (f in whatFolder)
if not file.isVisible(f)
foundAny = true
break
s = "The folder " + whatFolder + " contains "
if foundAny
s = s + "at least one invisible file or folder."
else
s = s + "no invisible files or folders."
dialog.notify(s)
TIP The pathname of the absolute top-level "conceptual" folder is the empty string. Saying fileloop (f in "") will loop through the names of your mounted volumes.
There is a second syntax. The parentheses contain a second item, separated from the first by a comma; the second item is a positive integer indicating the depth to which the search on disk should be recursively performed. If the depth is 1, we will look only within the folder named by folderPathname. If the depth is 2, we will look within the folder named by folderPathname, and also within any folders it contains. And so on. If the depth is infinity, we will look as deep as there are files within folders. It is not an error to assign too large a depth.
With this second form of fileloop(), there's a catch. Only names of files - not folders - will be assigned to f. Thus, these two statements do not quite do the same thing:
fileloop (f in folderPathname)
fileloop (f in folderPathname, 1)
They both assign f pathnames of just the immediate contents of folderPathname; but in the first case, these will be the pathnames both of folders and of files, whereas in the second case, they will be the pathnames of files only.
Here, for example, is a script that lets the user select a folder and then tells the name of the first invisible file (but not folder!) encountered at any depth within it:
local (f, whatFolder, foundAny = false, s)
if not file.getFolderDialog ("Please choose a folder.", @whatFolder)
return
fileloop (f in whatFolder, infinity)
if not file.isVisible(f)
foundAny = true
break
s = "The folder " + whatFolder + " contains "
if foundAny
s = s + "at least one invisible file, " + f + "."
else
s = s + "no invisible files at any depth."
dialog.notify(s)
The question then arises of how to get f to receive the names of both files and folders, yet pursue the search to a depth greater than 1. One answer is to use the first syntax of fileloop() but write a recursive handler; that is, we perform the recursion ourselves rather than asking fileloop() to do it. The technique is so common that its basic structure is worth memorizing; it is then easy to build any particular functionality around it.
on traverse (path) |
fileloop (f in path) |
if file.isFolder (f) « dive into the folder |
traverse (f) « recurse |
For example, here is a routine to let the user select a folder and then tell the name of the first invisible file or folder encountered at any depth within it.
Here we've made some slight adjustments to take account of the recursive nature of things. We want to stop as soon as we hit the first invisible file or folder, but we can't just break out of the loop: we have to unwind the recursion all the way up to the surface. And, to retain the name of the invisible file or folder, we store it in a global, whatF, because f after traverse() unwinds completely will be the f of surface level, not the f that caused us to stop.
A particularly beautiful and educational example of traverse() (and UserTalk recursion in general) is suites.samples.basicStuff.buildFolderOutline() , which constructs an outline whose hierarchy reflects the hierarchy of folders and files within a user-designated folder.
If writing your own recursive handler scares you, or you just can't be bothered, you can use file.visitFolder() instead (see later in this chapter).
A visit construct is a looping construct rather like for...in or fileloop(): we start with a set of similar entities, and Frontier loops over them for us, handing us each entity in turn. The calling syntax is a bit tricky. A visit verb is an ordinary script,1 not a keyword, so the commands to be executed during each loop cannot be indented below it. Instead, those commands are encapsulated in a handler or script whose address is handed as a parameter to the visit verb (see Chapter 8, Addresses).
In the discussion that follows, I shall refer to the handler whose address is passed as a parameter as "the proc"; it is often referred to in Frontier circles as a "callback," but I regard this as an incorrect use of the term. I shall refer to the set of similar entities over which a visit verb loops as its "domain."
The proc must return a boolean, or something that can be coerced to a boolean. The visit verb examines the proc's result after each iteration, and proceeds to the next iteration only if it is not false.
Further, the proc must take exactly one parameter in which to receive each entity in turn. The only exception is the proc for op.visit(); it takes no parameters.
There is something of an art to writing a proc, because it isn't itself a loop: it is called anew for each iteration. Thus, it cannot maintain state within itself. How, for example, might the proc determine how many times it has been called so far? A common solution is to employ variables global to the proc, where state can be maintained; we shall see an example of this immediately.
This construct takes one parameter, the address of the proc. The domain is the set of all open windows within the Frontier application. The parameter passed to the proc each time is a reference to an open window; the windows are traversed in front-to-back order (window references are discussed in more detail in Chapter 24, Windows).
For example, here is a script that arranges all open Frontier windows into a cascade.
The script defines the proc, and then just hands it to window.visit(). Notice the use of variables cascH and cascV global to the proc; this allows their values to be maintained between calls to the proc.
In this routine we treat the Main Window specially: we want it located first; it has no titlebar, so its "position" is its actual upper left-hand corner, whereas a normal window's position is the upper left-hand corner of its content region. We check where each window was actually placed, because Frontier will override our instructions if we try to use window.setPosition() to move the right edge of a window off the screen.
This construct takes two parameters: the address of the stack and the address of the proc. The domain is all the values in the stack pointed to by first parameter. Thus, the proc has no opportunity to alter any values in the stack; it is receiving values, not addresses. Stacks are described in Chapter 22, Stacks.
This construct takes one parameter, the address of the proc; the proc should take no parameters. It is assumed that the "target" (the outline to be worked on) has already been set, and that a desired line of that outline has already been selected; op.visit() selects each subhead of that line and calls the proc, which can then use op verbs that act upon the currently selected line. (The "target" and the op verbs are discussed in Chapter 18, Non-Scalars.)
Not only subheads of the originally selected line are traversed, but also subheads of subheads. But the expansion state is not changed. So the domain of the verb is all the subheads of the originally selected line, to any depth at which they are currently expanded. A line is selected, and the proc is called, before the first of that line's expanded subheads (if any) is selected.
The originally selected line is not among those for which the proc is called. The original position of the cursor is not restored at the end; this is up to the calling routine, if desired.
In this dramatic and somewhat useless example, we make an outline containing the names of the 50 states of the United States and then delete any whose name contains the letter "e":
on deleteWithE()
if op.getLineText() contains "e"
op.deleteLine()
bundle « create the outline
new (outlineType, @workspace.theStates)
edit(@workspace.theStates)
op.wipe(); op.setLineText("The States")
loop (local (x = 1); x <= 50; x++)
op.insert(states.nthState(x), down)
op.go(up, infinity); op.demote()
op.visit(@deleteWithE)
The bundle material just makes the outline for us to work on; the only interesting part (for our purposes) is the proc, deleteWithE(). Notice that op.visit() is clever enough to deal with the possibility that the currently selected line might be deleted by the proc.
This construct takes two parameters: the address of a table and the address of the proc. Its domain is first, the address of the original table, and second, the address of each entry in that table, including the address of each entry in any subtables, and so on. The proc always receives a table just before its entries.
Often what one wants to do is process a table's contents but not the table itself. It is therefore useful to be able to avoid having the proc process the first value handed to it. Use of a variable global to the proc can help accomplish this.
In this example we count the number of scripts in the suites table, at any depth. For illustrative purposes, we show how to avoid processing the suites table itself, though there is no actual need to do so. (The technique slows down processing, since an extra check must be made on each call to the proc; but this is unavoidable.)
local (ct = 0, firstTime = true)
on isScript(addrThing)
if firstTime
firstTime = false
return true
msg (addrThing) « provide some feedback
if typeOf(addrThing^) == scriptType
ct++
return true
table.visit(@suites, @isScript)
dialog.notify ("The Suites table contains " + ct + " scripts.")
This verb is an alternative to a fileloop(). It takes three parameters: the pathname of a folder, an integer representing the depth of recursion to be used, and the address of the proc. Unlike fileloop() used with a depth specifier, the proc receives the names of both files and folders. Files in a folder are received just before that folder; this is the reverse of table.visit().
Here is a rewrite of Example 12-3 using file.visitFolder():
local (whatFolder, whatF = false)
if not file.getFolderDialog ("Please choose a folder.", @whatFolder)
return
on checkVisibility (f)
if not file.isVisible (f)
whatF = f
return false « signal to stop
return true « otherwise keep looking
file.visitFolder(whatFolder, infinity, @checkVisibility)
s = "The folder " + whatFolder + " contains "
if whatF
s = s + "at least one invisible file or folder, " + whatF + "."
else
s = s + "no invisible files or folders at any depth."
dialog.notify(s)
Conditional constructs allow a bifurcation in the sequence of command execution: depending on a test performed at runtime, this set of commands or that set of commands will be performed.
There are two forms: if may be used alone, or it may be used with else. The rest of the if line must evaluate to, or be coercible to, a boolean. For operators that can be used in boolean expressions, see Chapter 44, Operators.
The first form looks like this:
if boolean
« what to do if so
« later stuff
The indented bundle will be executed only if the boolean is true.
In the second form, else appears as the first line following the if line at the same level, like this:2
if boolean
« what to do if so
else
« what to do if not
« later stuff
In this form, if the boolean is true, we execute the if's indented bundle, then skip the else (and its indented bundle) and proceed to the "later stuff "; but if the boolean is false, we execute the else's indented bundle before proceeding to the "later stuff."
The case construct is a way of specifying different actions depending upon various discrete values that an expression might have at runtime. This example, although by no means how one would optimally accomplish the particular task, illustrates the basic syntax clearly:
local
howMany = dialog.threeWay ("How many times would you like me to beep?", \
"Once", "Twice", "Three times")
case howMany
1
speaker.beep()
2
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
3
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
As you can see, what follows case on its own line is an expression to be evaluated; then, one level indented, there are possible values for that expression, and, indented from those, what to do if the expression matches that value.3 If more than one discrete value is to have the same action, you can list multiple values on one line separated by semicolons, or on successive lines like this:
local
howMany = dialog.threeWay ("How many times would you like me to beep?", \
"Twice", "Two Times", "Three times")
case howMany
1
2
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
3
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
clock.waitSixtieths(10)
speaker.beep()
An optional else may follow immediately at the same level as the case line; its indented bundle will be executed if none of the listed values was matched.
case will not accept ranges as values: the match between the expression and a value must be one of equality. However, a trick lets you take advantage of case syntax to build what amounts to a multiple if...else: use case true, and then list booleans as values. Since the booleans are tested in the order they appear, this is the equivalent of the if...else if...else if...else construct supported by many languages:
local (whatNum)
if not dialog.ask("Pick an integer from 1 to 10.", @whatNum)
return
whatNum = double(whatNum)
case true
whatNum < 1
dialog.alert ("That's too small.")
whatNum > 10
dialog.alert ("That's too large.")
whatNum ≠ long(whatNum)
dialog.alert ("That isn't an integer.")
else
dialog.notify ("Good, you can follow directions.")
If we reach the third case, the number is between 1 and 10 inclusive.
Because of a bug, a script will refuse to compile if it contains a case construct whose last value is commented out.
An error during the execution of a script means that the script cannot continue, but UserTalk makes it possible to keep going anyway, or at least to come to a halt in an orderly fashion. In fact, we shall see that your script can deliberately cause an error as a means of determining what happens next.
If a runtime error occurs, Frontier puts up an error dialog and execution comes to a halt. Sometimes, though, an error is not worth stopping your whole script for. You can prevent a halt in execution by trapping for errors; your script remains in control even if an error occurs. This is done by placing any commands that you think might generate an error in a try bundle.
There may or may not be an else immediately following at the same level as the try. If there is no else, then if a command in the try bundle generates an error, the rest of the try bundle is skipped, and we proceed to the first command after the try. If there is an else, then if a command in the try bundle generates an error, the rest of the try bundle is skipped and we execute the else bundle, and then proceed; if there is no error generated in the try bundle, the else bundle is never executed.
A typical place for using try alone is in a loop where a non-fatal error can result - you don't care about the error, you just want to go on to the next iteration of the loop. For instance, the script at samples.basicStuff.reallyEmptyTrash uses a fileloop() to go through everything in the Trash folder and erase it. If an error occurs, it's just because the file is somehow in use; we don't mind leaving that file alone, but we'd like to continue with the loop. So the attempt at deletion is in a try bundle:
local (vol, f, trashfolder)
fileloop (vol in "")
trashfolder = vol + "Trash:"
try
fileloop (f in trashfolder)
try
if file.isFolder
file.deleteFolder (f)
else
file.delete (f)
Another common use of try alone is where you don't want to check for the existence of something whose nonexistence is unimportant but would cause an error.
For example, in this line from toys.listToOutline:
try {delete (@scratchpad.xxx)}
we want to delete the database entry scratchpad.xxx if it exists. If it doesn't exist, asking to delete it will raise an error, but we don't care because then there's nothing to delete anyway.
The else option might be used in case the error means that execution can't proceed without mopping something up, or to allow smooth reporting or analysis of the error. We'll look at some examples in a moment.4
A call to scriptError() actually causes an error. The verb's parameter is a string; if the error it causes is not trapped by a try, Frontier puts up its usual error dialog, containing that string.
Why would anyone want to do something like this? One reason might be simply that we have in fact ended up in a fatal situation, and need to abort. It isn't sufficient merely to return; that will just put the caller of the present handler back in charge. What we want to do is stop all activity, and supply an informative error message as we do so.
For example, in this snippet from backups.backuproot() , we try to create a folder using file.sureFolder(); we know if this fails because the call returns false. So we test for that, and throw an error if it does:
if not file.sureFolder (backupfolder) «couldn't create the folder
scriptError ("Couldn't create the backup folder" + \
"or a file named 'backups' already exists.")
Here is another example snippet, simplified from html.data.standardMacros.renderObject(). The script is trying to set the value of addrScript; it has been given something called name, which is a kind of partial reference, and makes several guesses as to where in the database name might live. If none of them works, the whole operation needs to come to a halt:
addrScript = address (name)
if defined (addrScript^)
return
addrScript = @html.data.page.tools^.[name]
if defined (addrScript^)
return
addrScript = @user.html.renderers.[name]
if defined (addrScript^)
return
scriptError ("Can't find a rendering script named \"" + name + "\".")
Another reason for throwing our own error is that a fatal error has already occurred, but we think we can make a more informative error dialog than the original error does. The original error is trapped in a try, and we substitute our own error message in the corresponding else.
In this example snippet, again simplified from renderObject(), we have been trying, in a case construct, to deal with an entity (pointed at by addr) in various ways, depending upon its type. Finally, in the case's corresponding else, we make one last-ditch effort: we coerce addr^ to a string. If that fails, we trap the error and substitute our own error message for the one Frontier proposes to generate:
try
s = string (addr^)
else
theError = "Can't render \"" + string (addr) + "\" because " + \
" its type is \"" + objecttype + "\""
scriptError (theError)
It frequently happens that we wish to know what the error message would have been for an error that has been trapped within a try (this error message will have been generated by Frontier or some application it was talking to, or by a call to scriptError()) if we hadn't trapped it in the try. Access to the error message generated in a try bundle is available in the corresponding else bundle through a global system variable tryError .5
In this example snippet from html.ftpText() we attempt to upload a file via FTP; if this fails, we examine the error message generated. If it has to do with memory, the situation is fatal, and we just pass the error along unchanged; otherwise, we assume a needed directory doesn't exist, so we create it and try again:
try
ftpClient.store (f, domain, path, account, password)
else
if tryError contains "memory"
scriptError (tryError)
else
ftpClient.sureFilePath (domain, path, account, password)
ftpClient.store (f, domain, path, account, password)
This verb causes your system to, well, crash. If you have a system-level debugger, such as MacsBug, Frontier drops into the debugger, and, if syscrash() was given a string parameter, that string is displayed. This can be used for last-ditch debugging if Frontier itself is having some kind of problem (for example, I once used it to detect a bug in the try...else mechanism); but generally it is of no interest to UserTalk scripters, since Frontier's own debugging facilities will suffice. These are discussed in Chapter 13, Running and Debugging Scripts.
2. It is a compile-time error for else to appear anywhere but in this relationship to an if (or case, or try).
3. Unlike C, it is not true that one "falls" from one case into the next unless prevented by a break.
TOP | UP: UserTalk Basics | NEXT: Running and Debugging Scripts |