DekGenius.com
[ Team LiB ] Previous Section Next Section

9.6 Compiled Script Files as Script Objects

A script can read a compiled script file and incorporate its contents as a script object. This provides a way for scripts in different files to refer to one another. You might use this facility as a means of persistent storage, in combination with the fact that top-level entities in scripts survive being saved as a compiled script file; or you might use it as a way of building a library of commonly needed routines.

This facility depends upon three verbs, described here, that are not part of AppleScript proper; they are implemented in a scripting addition (Chapter 4) that is standard on all machines.

load script

Syntax

load script aliasOrFile

Description

Returns the top-level script of the compiled script file aliasOrFile as a script object.

Example

set myScript to load script alias "myDisk:myFile"
run script

Syntax

run script aliasOrFile [with parameters list]

Description

Tells the top-level script of the compiled script file or text file aliasOrFile to run, optionally handing it the list as the parameters for its explicit run handler, and returns the result.

Example

run script alias "myDisk:myFile"
store script

Syntax

store script scriptObject [in file path [replacing yes|no]]

Description

Saves scriptObject to disk as a compiled script file. Returns no value. If no further parameters are supplied, presents a Save File dialog; if the user cancels, a runtime error is raised. If path is supplied, presents no Save File dialog, but if the file exists already, presents a dialog asking how to proceed; if the user cancels, a runtime error is raised. If replacing is supplied, this dialog is suppressed; if yes, the file is just saved, and if no, an error is raised if the file exists. The filename extension determines the format of the resulting file: .scpt (or nothing) for a compiled script file, .scptd for a script bundle, .app for an application bundle.

Example

store script sayHello in file "myDisk:myFile" replacing yes

(On aliases and file specifiers and the differences between them, see Chapter 13. The verb run script, instead of a file, can take a string, and it then functions as a kind of second level of evaluation; see Chapter 12.)

When you save a script object with store script, the lines delimiting the definition block (if any) are stripped, which makes sense. So, for example:

script sayHello
        display dialog "Hello"
end script
store script sayHello in file "myDisk:myFile" replacing yes

What is saved in myFile is the single line:

display dialog "Hello"

A compiled script file to be loaded with load script or run with run script could originate from a store script command, or it could have been saved directly from a script editor program. A text file to be run with run script could originate from any word processor that can save as text.

The run script command permits a run handler to have parameters. (See Section 8.5.3.) For example, suppose you save this script as myScript.scpt:

on run {greeting}
        display dialog greeting
end run

You can't run that script on its own, but you can run it by way of run script, because this command can pass the needed parameter to the run handler:

run script file "myDisk:myScript.scpt" with parameters {"Hello"}

9.6.1 Library

A compiled script file may be used as a place to store commonly needed routines. A file used in this way is called a library. A running script can then access the contents of the library using load script. The library's top-level entities, including its run handler, are then available to the running script.

For example, suppose we have saved the handler makeFilterer (see the end of Section 9.5.3) in a compiled script file makeFilterer.scpt. We can then call makeFilterer from another script:

set s to load script file "myDisk:makeFilterer.scpt"
on isNumber(x)
        return ({class of x} is in {real, integer, number})
end isNumber
tell s's makeFilterer(isNumber) to filter ({"hey", 1, "ho", 2, 3})

That code assigns the entire script of the compiled script file makeFilterer.scpt to a variable s. Then the handler makeFilterer is accessed by way of s. Alternatively, since the compiled script file's top-level entities are available to us, we could have extracted the handler makeFilterer from the compiled script file makeFilterer.scpt and assigned it to a variable:

set makeFilterer to makeFilterer of (load script file "myDisk:makeFilterer.scpt")
on isNumber(x)
        return ({class of x} is in {real, integer, number})
end isNumber
tell makeFilterer(isNumber) to filter ({"hey", 1, "ho", 2, 3})

The advantage of a library is that it makes code reusable and maintainable. In this example, makeFilterer is a very useful handler. We don't want to have to keep copying and pasting it into different scripts. If its code lives in a library, it becomes accessible to any script we write. Furthermore, as we improve makeFilterer in its library file, those improvements are accessible to any script; a script that already calls makeFilterer by way of load script simply inherits the improvements the next time it runs.

On the other hand, a library reduces portability. In this case, we cannot just copy a script that calls makeFilterer to another machine, or send it to a friend, because it depends on another file, makeFilterer.scpt, and refers to it by a pathname that won't work on any other machine.

With Script Debugger, a trick for working around this problem is to load any library files as part of your script property initialization:

property makeFilterer : makeFilterer of (load script file "myDisk:makeFilterer.scpt")
on isNumber(x)
        return ({class of x} is in {real, integer, number})
end isNumber
tell makeFilterer(isNumber) to filter ({"hey", 1, "ho", 2, 3})

That code loads the compiled script file makeFilterer.scpt and initializes the property makeFilterer to the bytecode of the script file's handler makeFilterer—but only when the property makeFilterer needs initializing. After that, the handler is persistently stored as the value of the property makeFilterer. (Script Editor no longer performs this kind of persistent storage of properties; that's why this trick won't work with Script Editor. See Section 7.6.)

A script file created in this way with Script Debugger can be distributed to other machines, and it will still run. It must not, however, be edited on another machine! If the user on another machine edits the script and tries to compile it, the script is ruined: the value of the property makeFilterer is thrown away, AppleScript will try to reinitialize it, the load script command will fail because the file it refers to doesn't exist, and the script will no longer compile or run. In fact, the script is ruined if it is so much as opened with Script Editor. Script Debugger also helps you in this situation by allowing you to "flatten" a script so that it incorporates all library files on which it depends, and so has no load script dependencies.

9.6.2 Data Storage

We can use store script to take advantage of the persistence of top-level script object entity values (Section 9.2.2, earlier in this chapter). This can be a way of storing data on disk separately from the script we're actually running. You might say: "Why bother? Persistent data can be stored in the script we're actually running." Well, that's true for such environments as Script Debugger, or an applet; but it isn't true for the Script Editor. Besides, in any environment, persistence within a script comes to an end as soon as we edit and recompile the script. Storing the data separately circumvents such limitations.

In this example, we start by ascertaining the user's favorite color. This will be kept in a file myPrefs. The first thing we do is try to load this file. If we succeed, fine; if we fail, we ask the user for her favorite color and store it in myPrefs. Either way, we now know the user's favorite color, and we display it; and the information is now in the file myPrefs, ready for the next time we run the script. (See Section 24.1.4 for a variant of this example using an application bundle.)

set thePath to "myDisk:myPrefs"
script myPrefs
        property favoriteColor : ""
end script
try
        set myPrefs to load script file thePath
on error
        set favoriteColor of myPrefs to text returned of ¬
                (display dialog "Favorite Color:" default answer ¬
                        "" buttons {"OK"} default button "OK")
        store script myPrefs in file thePath replacing yes
end try
display dialog "Your favorite color is " & favoriteColor of myPrefs

If you run that script, entering a favorite color when asked for it, and then open the file myPrefs in a script editor program, you may be surprised to find that it doesn't actually seem to contain your favorite color:

property favoriteColor : ""

Don't worry! The information is there; it simply isn't shown in the decompiled version of the script. The decompiled version shows the actual bytecode, not the table of persistent data stored internally with the script.[1]

[1] The only way I know of to get a look at the persistent data stored internally with a script is to use Script Debugger. Script Editor doesn't show it, and destroys it if you open and run the script directly.

If you load a script as a script object with the load script command, and top-level entity values within this script object change, and you wish to write these changes back to disk, it is up to you do so, with store script.

The run script command does not save the script, so any changes in the script's top-level entity values do not persist.

9.6.3 Context

Recall from earlier in this chapter (Section 9.5.3.1) that a script object carries with it a memory of its global context. This applies when a script object is saved with store script and inserted into a different context with load script. As we've already said, there's more to a compiled script file than meets the eye; there's the decompilable bytecode, and there's the persistent data stored internally with the script. The persistent data isn't visible, but it is the context in which the script runs. The store script command saves a context into the compiled script file that it creates; the load script command loads this context, and the run script command runs within it.

The context comes into play when the script object refers to variables defined at a higher level (free variables). In particular:

  • If a script object refers to a top-level global, then when the script object is loaded into another context with load script, the fact of the global variable is remembered but its value must be supplied by a global variable with the same name in the new context. No global declaration is needed.

  • With load script, all other types of higher-level variable referred to in a script object simply keep the value they had when the context was saved.

  • With run script, all higher-level variables referred to in a script object keep the value they had when the context was saved.

Suppose, for example, you run this script:

set thePath to "myDisk:myMessage"
global message
set message to "Howdy"
script myMessage
        display dialog message
end script
store script myMessage in file thePath replacing yes

The script object myMessage contains a reference to a top-level global (the variable message). That script object is saved into the file myMessage. Now we load myMessage into a different script, like this:

set thePath to "myDisk:myMessage"
set message to "Hello"
run (load script file thePath) -- Hello

The dialog appears, but it says Hello (not Howdy). The reference to a global variable message within myMessage adopts the value of the top-level global message in this new context, even without an explicit global declaration. But if we load myMessage into a context where there is no global message for it to identify its free variable message with, we get an error:

set thePath to "myDisk:myMessage"
run (load script file thePath) -- error

If we run the same myMessage with run script, on the other hand, it works even without a global message in the new context, because the script object remembers the global message from its original context:

set thePath to "myDisk:myMessage"
run script file thePath -- Howdy

Now we'll start all over again, generating a completely new myMessage file. This time the free variable message is identified with a property:

set thePath to "myDisk:myMessage"
property message : "Get lost"
script myMessage
        display dialog message
end script
store script myMessage in file thePath replacing yes

Now, this works:

set thePath to "myDisk:myMessage"
run (load script file thePath) -- Get lost

The script object has conserved the original context for the free variable message—both the property message and its value. The same is true when the original context involves a handler or script object. Suppose we generate myMessage like this:

set thePath to "myDisk:myMessage"
on sayHowdy(  )
        display dialog "Howdy"
end sayHowdy
script myMessage
        sayHowdy(  )
end script
store script myMessage in file thePath replacing yes

Then this works:

set thePath to "myDisk:myMessage"
run (load script file thePath) -- Howdy

We did not explicitly save the handler sayHowdy, but it was referred to in the script object myMessage, so it was stored as part of myMessage's context, and is present when we load myMessage into another script.

    [ Team LiB ] Previous Section Next Section