You’re looking at a draft of a chapter from a work in progress, tentatively titled Scripting Mac Applications With Ruby: An AppleScript Alternative, by Matt Neuburg.
Covers rb-appscript 0.6.1. Last revised Jun 23, 2012. All content ©2012 by the author, all rights reserved.


Prev: Properties and Elements
Next: Datatypes
Contents

Chapter 6: Commands

1. Command Parameters

2. Canonical Form and Convenience Form

3. Repetition of the Application Reference

4. Get and Set

5. Multiple References as Direct Parameter

6. Make and Insertion Locations

7. Count

8. Timeout

9. No Waiting For Reply

10. Built-In Commands

10.1. Launch

10.2. Run

10.3. Activate

10.4. Reopen

10.5. Open

10.6. Print

10.7. Quit

In the previous chapter, we saw that it is possible to form a reference to an object in the scriptable application’s world without actually sending an Apple event to the scriptable application. What does send an Apple event to the scriptable application is your use of a command. Objects, together with properties and elements, may be thought of as the nouns and adjectives of scriptability; commands are the verbs of scriptability. If you look back in Chapter 3, where we analyzed a raw Apple event, you’ll see that the contents of the Apple event started with two four-letter codes, hookPlay, signifying the command. Every Apple event has exactly one command; indeed, the link between a command and an Apple event is so tight that a command is often called an event.

So far in this book, our repertory of commands has been deliberately kept very small — in fact, we’ve confined ourselves almost entirely to the command get — and there has been no rigorous discussion of what a command is and how its syntax operates. This chapter will remedy that. Here you’ll find a complete explanation of commands and their syntax, so that you’ll be able to use any command.

1. Command Parameters

A command is defined (and, in the scriptable application’s dictionary, is described) as having some number of parameters. The number of parameters may be zero, but if it is non-zero, every parameter has a name, signified (of course) by a four-letter code — except that very often, there will be one parameter without a name. This is the so-called direct parameter (or direct object), the one whose four-letter code is '----'. (Again, see Chapter 3, where this point arose.)

Thus we may distinguish the direct parameter, on the one hand, from the named parameters, on the other. And rb-appscript draws exactly this distinction. From the Ruby (rb-appscript) point of view, a command has at most two parameters: the direct parameter, and a hash consisting of the named parameters. The names of the named parameters (the keys to the hash) are represented as symbols.

Every parameter may also be described as required or optional, but this is purely informational — this is, it is not strictly speaking a matter of syntax whether a parameter is included, but rather it is up to the individual scriptable application to decide how to interpret a command if values for some of its parameters are not provided. Take, for example, the iTunes play command. It takes two parameters. One is the direct parameter, signifying the track that is to be played. The other is a named parameter called :once, a boolean signifying whether the track should be played singly and then iTunes should stop, or whether iTunes should “go on” in some appropriate sense when the end of the track is reached (usually this means that iTunes will go on to play the next track in the same playlist). Both parameters are optional, and to prove this, all you have to do is omit one or both of them. It turns out that if you omit the :once parameter, iTunes behaves as if it were false; and if you omit the direct parameter, iTunes plays something appropriate if it can — the current track, or the current selection, or the first track of the current playlist. The details, however, are unimportant here; the point is only that the behavior of iTunes or any application in response to a command, and to the presence or absence of its parameters, is an arbitrary matter regarding the individual scriptable application, and typically must be discovered by experimentation.

What we are concerned about in this chapter, then, is how you issue a command, and how you specify the parameters that you do provide.

2. Canonical Form and Convenience Form

In rb-appscript, commands may typically be issued in either of two forms: a canonical form and a secondary, convenience form. We’ll start with the canonical form (because it is canonical) and then proceed to the convenience form, which is the one you are most likely to use (because it is convenient). In both forms, the command is a method call whose name is the name of the command. The difference lies in what object the method call is sent to and in how the parameters are arranged.

In the canonical form, the command is a method call sent to the :application object. If there are both a direct parameter and a named-parameter hash, the direct parameter is first and the named-parameter hash is second. So, for example:

itu = Appscript.app("iTunes.app")
itu.play(itu.playlists["blues"].tracks["Broke and Hungry Blues"], {:once => true})

Ruby being Ruby, you can omit the curly braces around the literal hash, because it is the last parameter:

itu.play(itu.playlists["blues"].tracks["Broke and Hungry Blues"], :once => true)

And you can omit the parentheses around the parameters:

itu.play itu.playlists["blues"].tracks["Broke and Hungry Blues"], :once => true

But all of that is just syntactic sugar, and has no bearing on the Apple event.

It is legal to omit one or both parameters. (I mean legal syntactically; what a particular scriptable application will do in response to your inclusion or omission of parameters is, as I’ve already said, a different matter.) If there is just one parameter, then rb-appscript examines its class; if it’s a hash, rb-appscript assumes that it is the named parameter hash and that you are omitting any direct parameter. This means that if the direct parameter is itself a hash, then you must provide a second parameter — which can be an empty hash if no named parameters are to be supplied to the Apple event — so that rb-appscript will understand that the direct parameter is the direct parameter! (Fortunately this situation arises so rarely that I can’t even think of an example.)

In the convenience form, the command is a method call sent to the object reference representing the direct parameter. (Naturally, this is possible only if the command has a direct parameter.) The named-parameter hash, if any, is the method call’s single parameter.

itu = Appscript.app("iTunes.app")
itu.playlists["blues"].tracks["Broke and Hungry Blues"].play :once => true

It will be readily seen that the convenience form really is more convenient, if only because you don’t need two representations of the :application object (one to send the command to, and one to form the reference to the direct parameter). On the other hand the canonical form is arguably clearer.

A few common commands have additional convenience forms of their own in rb-appscript, and we’ll deal with these in a moment.

3. Repetition of the Application Reference

When issuing a command in canonical form, there is often (as we have just seen) a repetition of the application reference:

itu = Appscript.app("iTunes.app")
itu.play(itu.playlists["blues"].tracks["Broke and Hungry Blues"], {:once => true})

That works because the variable itu is already an application reference (an Appscript::Application instance) before we reach the second line. We could not do this, however, if there were no first line, and the script started out like this:

 Appscript.app("iTunes.app").play( # ... now what?

What should come next? We do not want to say Appscript.app("iTunes.app") again, since this will generate a new reference to the application. What we want is a way to say “a reference to the application I’m already talking to”. And indeed, rb-appscript gives us a way to do exactly this: use the bare method Appscript.app with no parameters.

Appscript.app("iTunes.app").play(Appscript.app.playlists["blues"].tracks["Broke and Hungry Blues"])

4. Get and Set

Along with get, which we’ve already seen used extensively, the most important Apple event command is set, which is used to change the value of a property. Indeed, in a scriptable application with a well-constructed object model, it is often the case that most of the actions you might be interested in taking are exposed in the form of properties whose values you can change, so that get and set together allow you to do most of what you want the application to do, with little need for other commands.

The set command takes a direct parameter (a reference to the thing whose value you want to change) and a :to parameter (the new value). So, in canonical form, we could change the name of an iTunes playlist like this:

itu = Appscript.app("iTunes.app")
itu.set(itu.playlists["blues"].name, {:to => "laments"})

But you can, of course, use the convenience form, and when you do, rb-appscript provides an additional convenience: you can supply the :to value directly, like this:

itu = Appscript.app("iTunes.app")
itu.playlists["blues"].name.set "laments"

In other words, if set has just one parameter, rb-appscript assumes that this is the :to parameter (and that you are using the convenience form).

In some rare cases, you must explicitly specify the :to parameter — namely, when the value you want to pass to the set command is a hash. This is because if you omit the :to parameter, the hash is taken to be the set command’s named parameter hash. So, to use a completely artificial made-up example, suppose there were a scriptable application where you tried to say this:

fakeapp = Applescript.app("FakeApp.app") # not real example
fakeapp.someproperty.set {:what => "this"}

You’d get an error, because this is interpreted as meaning that you are trying to specify the :what parameter of the set command — and the set command doesn’t have a :what parameter. So you have to state explicitly that you are specifying the :to parameter:

fakeapp = Applescript.app("FakeApp.app") # still not real example, but right way
fakeapp.someproperty.set({:to => {:what => "this"}}) # or...
fakeapp.someproperty.set(:to => {:what => "this"}) # or...
fakeapp.set( fakeapp.someproperty, :to => {:what => "this"} ) # and so on

Not every property is settable, and if you try to set the value of a non-settable property, the scriptable application will complain (by raising an exception). A property like this is called read-only, and the dictionary is supposed to distinguish read-only properties from settable properties.

itu = Appscript.app("iTunes.app")
itu.current_playlist.set itu.playlists["blues"]
#=> Appscript::CommandError: CommandError OSERROR: -1708 MESSAGE: Application could not handle this command.

The get command generally takes just the direct parameter, a reference to the thing whose value is desired. Once in a while, though, you may want the scriptable application to return from get a value whose datatype is different from what it would normally return. In this situation, add the named parameter :result_type, whose value is a symbol representing the desired datatype (a class_). You can actually do this with any command, but it is rare with any command other than get. Different scriptable applications respond to this sort of request (called a coercion) in different ways.

f = Appscript.app("Finder.app")
p f.selection.get
#=> [app("...Finder.app").startup_disk.folders["Users"].folders["mattleopard"].
#=>   folders["Desktop"].document_files["screenshot.png"]]
p f.selection.get( :result_type => :string )
#=> ["Hume:Users:mattleopard:Desktop:screenshot.png"]

As that example shows, normally when we ask the Finder for its selection we get back an array of references in canonical form. But if we ask explicitly for a :string, the array contains pathname strings with colons as separators (this is the legacy Macintosh pathname structure, from before the days of Mac OS X).

5. Multiple References as Direct Parameter

Some applications will permit you to use multiple references as the direct parameter to a command. (See “Distribution Over Multiple Internal References” in Chapter 5.) This is a convenience, because instead of sending many Apple events, each with a single reference as its direct parameter, you send one Apple event with a whole bunch of references as its direct parameter. Commands where this might work are typically such things as set, move and delete.

For example, the Finder will permit us to distribute the label_index property over multiple files when we get its value:

f = Appscript.app("Finder")
p f.files[Appscript.its.name.begins_with("i")].label_index.get
#=> [0, 0], because there are two such files

But it will also permit us to distribute the label_index property over multiple files when we set its value:

f = Appscript.app("Finder")
f.files[Appscript.its.name.begins_with("i")].label_index.set 1
 # we have just set the label_index of two files with one command: here's proof...
p f.files[Appscript.its.name.begins_with("i")].label_index.get
#=> [1, 1], it worked!

Similarly, we can move multiple files simultaneously to the Trash:

f = Appscript.app("Finder")
f.files[Appscript.its.name.begins_with("i")].delete
 # two files were moved to the Trash

6. Make and Insertion Locations

Another widely used command is make, which creates a new object in the scriptable application’s world. It usually takes at least two named parameters (and no direct parameter) — a :new parameter giving the class_ of the object to create (required), and an :at parameter saying where to create it (optional).

The :at parameter is sometimes described as an insertion location; this is a type of reference I haven’t talked about before, and it is used for a couple of other commands as well (such as duplicate), so this is the moment to discuss it. There are two specifier forms unique to an insertion location:

But the :at parameter might not involve before or after or beginning or end; it could just be a reference to an object that can act as a container. And in many cases the :at parameter can be omitted. Different scriptable applications want their insertion locations specified in different ways, and the dictionary is not usually much help, so experimentation is generally needed; but the thing to keep in mind is that you’re creating an element, so what you want to do is explain to the scriptable application what this is to be an element of and perhaps how it is to be positioned in the array of elements that already exist. Where it is obvious what it’s an element of (e.g. the :application itself), the :at parameter can often be omitted.

Here are some simple cases. Here’s how to make a new folder on the desktop with the Finder:

f = Appscript.app("Finder.app")
f.make(:new => :folder, :at => f.desktop)

And here’s how to create a new document window in TextEdit:

te = Appscript.app("TextEdit.app")
te.make(:new => :document)

A third parameter to the make command is the :with_properties parameter. This is a hash whose keys are symbols representing the names of properties of the newly created object, and whose values are the corresponding values to be assigned to those properties as the object is created. For example, one would rarely just create a folder on the desktop, because the resulting folder is named “Untitled Folder”; it is more common to create a new folder and assign it a name, all in one move, like this:

f = Appscript.app("Finder.app")
f.make(:new => :folder, :at => f.desktop, :with_properties => {:name => "My Cool New Folder"})

Similarly we can create a new document window in TextEdit and put some text in that window, all in one move:

te = Appscript.app("TextEdit.app")
te.make(:new => :document, :with_properties => {:text => "Hello, World!"})

Sometimes :with_data is used instead of :with_properties; here, the newly created object has a single, simple value, and this is it. Here’s an example, also illustrating a more involved insertion location:

te = Appscript.app("TextEdit.app")
te.make(:new => :document, :with_properties => {:text => "Hello, World!"})
te.make(:new => :word, :with_data => "Wonderful ", :at => te.documents[1].words[1].after)
 # The document now says: Hello, Wonderful World!

An rb-appscript convenience form here is that the :at parameter can be omitted if the make command is sent to the insertion location.

f = Appscript.app("Finder.app")
f.desktop.make(:new => :folder)

te = Appscript.app("TextEdit.app")
te.make(:new => :document, :with_properties => {:text => "Hello, World!"})
te.documents[1].words[1].after.make(:new => :word, :with_data => "Wonderful ")

However, there is a difference between those two forms; in the second case, the insertion location is sent in the Apple event as a subj parameter rather than an insh parameter, and may not be properly understood by the target application. Hence, it is recommended that you stick with using an explicit :at.

7. Count

The count command asks the target application to report how many of something there are. It takes a direct parameter saying where to look (an object or element specifier), and an :each parameter specifying what class_ of object to count.

itu = Appscript.app("iTunes.app")
p itu.count(itu.playlists["blues"], :each => :track)

Naturally we can use the convenience form, where the count command is sent to the direct parameter:

itu = Appscript.app("iTunes.app")
p itu.playlists["blues"].count( :each => :track )

We can re-express this command by including tracks as part of the direct parameter and using a special minimal value for :each, namely :item.

itu = Appscript.app("iTunes.app")
p itu.playlists["blues"].tracks.count( :each => :item )

That, indeed, is the form that works best in most cases. Using it, you can count various sophisticated collections:

itu = Appscript.app("iTunes.app")
whose = Appscript.its
p itu.playlists["blues"].tracks[whose.artist.contains("Lemon")].count( :each => :item )

It happens that in some cases the :each parameter can be omitted:

te = Appscript.app("TextEdit.app")
p te.documents.count

However, many applications do not respond well to the omission of :each, and you will need to supply it, even if its value is just the minimal :item. (This is an important difference from AppleScript.)

Occasionally you may run into a scriptable application that is so badly behaved that its dictionary doesn’t define the term :each. In that case, count might not work no matter how you dance:

em = Appscript.app("Expression Media")
p em.windows[1].media_items.count
#=> Appscript::CommandError: CommandError OSERROR: -1700 MESSAGE: Can't make some data into the expected type. 
p em.windows[1].count(:each => :media_item)
#=> ArgumentError: Unknown keyword parameter: :each

To solve this problem, we can directly modify the application’s terminology to give it another command, count_, which defines the term :each:

em = Appscript.app("Expression Media")
em.AS_app_data.connect # important; ask rb-appscript to gather dictionary terminology
 # now we can inject a new command into the existing terminology
em.AS_app_data.reference_by_name[:count_] = [:command, ["corecnte", {:each=>"kocl"}]]
 # and now we can use our new command!
p em.windows[1].count_(:each => :media_item) #=> 2

I regard this problem as a bug in rb-appscript, actually; the reason count works with Expression Media in AppleScript is that AppleScript “injects” the term :each into every application’s dictionary, and rb-appscript should do the same, but doesn’t. However, applications as badly behaved as Expression Media in this example are extremely rare, and the bug is easily worked around by performing the injection ourselves, so it’s no big deal.

8. Timeout

As mentioned in Chapter 3, the sender of an Apple event can express a willingness to await a reply for a certain amount of time but no longer. If the target application doesn’t reply in the specified time, an exception is raised. The default is 60 seconds, but you can change it. To do so, supply a :timeout parameter whose value is the number of seconds you’re willing to wait. This might be necessary, for example, if the target application might need longer than 60 seconds to return a reply, but you’re willing to wait anyway. If you’re willing to wait forever (not usually a wise idea), supply a :timeout value of 0.

For example, on my machine the following script times out with an exception:

f = Appscript.app("Finder.app")
whose = Appscript.its
res = f.startup_disk.items["Users:mattleopard:desire"].entire_contents.application_files[
  whose.creator_type.eq("aplt")].get
#=> Appscript::CommandError: CommandError OSERROR: -1708 MESSAGE: Application could not handle this command.

That’s because it takes about 71 seconds to complete, which is longer than the default timeout. The solution is to provide a larger timeout value:

f = Appscript.app("Finder.app")
whose = Appscript.its
t = Time.now
res = f.startup_disk.items["Users:mattleopard:desire"].entire_contents.application_files[
  whose.creator_type.eq("aplt")].get :timeout => 120
p Time.now - t #=> 71.16718

9. No Waiting For Reply

As also mentioned in Chapter 3, the sender of an Apple event can express a desire not to wait around for any reply. To do this, supply a :wait_reply named parameter, with a value of false. The Apple event will return no value (nil), and your code will proceed immediately after the command.

Here’s a completely silly and impractical example, based on the previous example:

f = Appscript.app("Finder.app")
whose = Appscript.its
t = Time.now
res = f.startup_disk.items["Users:mattleopard:desire"].entire_contents.application_files[
  whose.creator_type.eq("aplt")].get :wait_reply => false
p Time.now - t #=> 0.021623

As you can see, we return immediately from a command that takes the Finder 71 seconds to complete (and, since we have been silly enough to ask for a result from that command, the result is nil). Our code proceeds, and meanwhile the Finder is tied up for the next 71 seconds, getting a list of files to no purpose. Although this particular example is silly, however, the ability to send a command off to a scriptable application and to proceed immediately can occasionally be useful.

10. Built-In Commands

A few basic commands (in addition to get and set), to which all scriptable applications are expected to respond, are built right into rb-appscript.

10.1. Launch

The launch command makes certain that an application is running, without causing it to come to the front or perform any of the actions that it normally performs when it starts up (such as creating a new document).

Suppose, for example, that TextEdit is not running. If you create an application reference to TextEdit and send it a command, TextEdit will be started up in the normal way, becoming frontmost and creating a single new document. The launch command is the sole exception. If you create an application reference to TextEdit, and the first thing you do with that reference is to send it the launch command, then TextEdit starts up in the background without creating any new document.

10.2. Run

The run command makes certain that an application is running, launching it (if it isn’t running) as if it had been double-clicked in the Finder. (Actually, it’s the other way around; double-clicking an application in the Finder sends that application the run command.) This might make it do things it wouldn’t do if you had sent it the launch command instead.

10.3. Activate

The activate command brings the target application to the front.

10.4. Reopen

The reopen command tells the target application to do whatever it normally does when it receives a reopen event. Okay, that’s tautological, but different applications respond to reopen events in different ways. To see how an application responds to a reopen event, click its icon in the Dock when the application is running; this sends the application a reopen event and you may be able to observe some special behavior. For example, TextEdit and the Finder respond by making sure that they have at least one window open (creating it if there isn’t one).

10.5. Open

The open command tells the target application to open one or more files, as if those files had been dropped on the application’s icon in the Finder. (Actually, it’s the other way around; dropping files on an application’s icon in the Finder sends that application the open command.) I’ll demonstrate, even though discussion of how to refer to a file doesn’t appear until the next chapter, by telling TextEdit to open whatever files are currently selected in the Finder:

te = Appscript.app("TextEdit.app")
te.open( Appscript.app("Finder.app").selection.get( :result_type => :alias ))

10.6. Print

The print command tells the target application to print one or more files, analogous to what happens when you select files in the Finder and choose File > Print (which — you guessed it — actually does send the relevant application a print command).

10.7. Quit

The quit command tells the target application to quit. The optional :saving parameter allows you to specify how the target application should deal with any open unsaved documents; its value is :yes, :no, or :ask. You are most likely to say :no, because if you are issuing the quit command without first dealing with any open unsaved documents individually, you are probably just trying to exit in good order. Document-based applications generally implement commands like close and save that let you dispose of a particular open document explicitly.


Prev: Properties and Elements
Next: Datatypes
Contents

You’re looking at a draft of a chapter from a work in progress, tentatively titled Scripting Mac Applications With Ruby: An AppleScript Alternative, by Matt Neuburg.
Covers rb-appscript 0.6.1. Last revised Jun 23, 2012. All content ©2012 by the author, all rights reserved.

This book took time and effort to write, and no traditional publisher would accept it. If it has been useful to you, please consider a small donation to my PayPal account (matt at tidbits dot com). Thanks!