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.
Chapter 6: Commands
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
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.
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.
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.
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"])
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).
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
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:
Relative to a specified element, using the before
or after
method:
documents[1].after
Relative to all of some kind of element, using the beginning
or end
methods with the element name:
documents.beginning # should be equivalent to documents[1].before
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
.
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.
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
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.
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.
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.
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.
The activate
command brings the target application to the front.
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).
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 ))
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).
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.
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!