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 5: Properties and Elements
1. Properties
6. Elements
7.1. All
7.2. By Index
7.3. By Name
7.4. By ID
7.5. By Position Name
7.6. By Range
7.7. By Relative Position
7.8. By Boolean Test
8. Distribution Over Multiple Internal References
The world of a scriptable application, as revealed and made accessible to us through Apple events, is a world of objects. We may imagine a scriptable application as dispensing objects to us; just about any Apple event we send to a scriptable application will need to specify such an object.
Much of the art of scripting a Mac application with Apple events is knowing what objects the application is willing to dispense, and how to specify a desired object. Even if you don’t particularly need an object as a value, you will still need to know how to refer to an object, in order to make your scriptable application do anything. We have already learned how to refer to one of the scriptable application’s objects — its representation of itself, the :application
object, which comes to us as an Appscript::Application
instance. (It will become clear later in this chapter why I speak of the :application
object using a Ruby symbol, a name starting with a colon.) This object, alone, can be made to do a few things, without reference to any other object; we can tell it, for example, to activate
, or to quit
. But the really interesting stuff doesn’t start happening until we can refer to other objects that the scriptable application is ready to dispense.
Let’s take iTunes as an example. Let’s say my copy of iTunes contains a song called “Broke and Hungry Blues”, and that I’d like to tell iTunes to play it. To do that, I need somehow to refer to the song “Broke and Hungry Blues.” How do I do that? Let’s go further. The song “Broke and Hungry Blues” has an artist. That, too is an object. How can I script iTunes to tell me the artist of the song “Broke and Hungry Blues”? You can see already that anything interesting we want to do with iTunes is going to involve objects beyond the mere :application
object. This chapter tells you about the nature of such objects and how to refer to them.
When I speak of an “object” in the world of a scriptable application, this hasn’t much to do with the notion of object-oriented programming, as embodied in a language like Ruby (though of course, because Ruby is object-oriented, if we ask a scriptable application to give us one of its objects, it will arrive as a Ruby object). But a scriptable application’s objects do have certain architectural similarities to Ruby objects. One such similarity is that an object in the world of a scriptable application can have attributes, which are somewhat like a Ruby object’s instance variables. Every attribute is either a property or an element. (In a sense, “attribute” is simply a made-up word intended to embrace the notions of property and element together.) What’s more, it turns out that the way we specify an object is in terms of some other object’s attributes — its properties and elements. So we need to know about properties and elements in order to get anything done in the world of a scriptable application. We need to know about them in order to understand the nature of a scriptable application’s objects, and we need to know about them in order to refer to a scriptable application’s objects. That’s why properties and elements are the subject of this chapter.
Also, an object in the world of a scriptable application has a “class,” though this is only vaguely similar to the Ruby notion of a class. As we’ll discover in this chapter, a scriptable application object’s “class” is little more than a list of what properties and elements it has.
A property is an object attribute that is a single named value. For example, every song in iTunes has an artist, much in the same way as we can imagine creating a Song class in Ruby whose methods refer to an @artist
instance variable. Just as we could then get and set a particular Song instance’s @artist
value (if the Song class has the appropriate accessors for the @artist
instance variable), so too we can get and set a particular song’s artist value in iTunes.
In rb-appscript, the way to refer to a property of an object in the world of a scriptable application is with a method whose name is the name of the property (somewhat analogous to an instance variable accessor). So, for example, if we did have a reference to a particular song in iTunes, and if this reference were in a variable, say, the_song
, then we could refer to its artist using an artist
method, like this:
the_song.artist
So from now on, when I want to speak of a property of an object, I’ll use the name of the method that you would send to that object using rb-appscript to refer to that property. For example, I’ll speak of an artist
property (using that typography), or of a song’s artist
.
get
We now know how to refer to a property; but how do we obtain that property’s value? The primary way is by sending it the get
message. In telling you this, I’m getting a little ahead of myself, because get
is a command, and formal discussion of commands doesn’t really start until the next chapter. But get
is such a useful thing to say, especially when you’re testing to see whether you’re successfully referring to a property, that we may as well start using it right now.
For example, an :application
object itself has properties, many of which can be quite interesting. The iTunes :application
object, for one, has many useful properties, such as player_state
, which tells whether iTunes is currently playing a song, or paused in the playing of a song, or not playing any song; and current_track
, which tells what song is currently playing or paused. But sending, say, the player_state
message to iTunes is not, of itself, a particularly useful thing to do:
itu = Appscript.app("iTunes.app")
itu.player_state
Hmmm, the script runs fine, but nothing happens. Perhaps this is because we didn’t ask for any output; let’s try puts
:
itu = Appscript.app("iTunes.app")
puts itu.player_state #=> app("/Applications/iTunes.app").player_state
That’s a response, but it isn’t very interesting; basically, we said player_state
and rb-appscript has just said player_state
right back at us. That’s because we’ve correctly formed a reference to the iTunes :application
object’s player_state
property, but that’s all we’ve done; we haven’t proceeded to access its value. To do so, we need to send the get
command to the property:
itu = Appscript.app("iTunes.app")
p itu.player_state.get #=> :paused
Now we’re cooking with gas! We have actually scripted iTunes to do something useful: we asked it a question and received the answer. So remember the magic word get
when you actually want to retrieve and examine a property’s value. I’ll be using it quite a lot throughout the remainder of this chapter.
When you say get
to a scriptable application and you receive a value, that value will fall broadly into one of two very different categories. We can illustrate this difference by contrasting the iTunes :application
object’s player_state
property value with its current_track
property value:
itu = Appscript.app("iTunes.app")
p itu.player_state.get
#=> :paused
puts itu.current_track.get
#=> app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(399).file_tracks.ID(412)
Intuitively we can see that these are very different sorts of thing. The value :paused
is simple. The value app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(399).file_tracks.ID(412)
looks more like a Ruby expression; it looks like the kind of thing we would say, in Ruby, to a scriptable application. And in fact that’s exactly what it is.
We can perceive the difference more rigorously by asking Ruby for the underlying class of each value:
itu = Appscript.app("iTunes.app")
puts itu.player_state.get.class #=> Symbol
puts itu.current_track.get.class #=> Appscript::Reference
Let’s call values of the first type copies, and values of the second type references. The Ruby class of a copy will vary; it might be String, or Fixnum, or Symbol, or any of a number of various other things. But the Ruby class of a reference will always be Appscript::Reference
(except for the :application
object, which is an Appscript::Application
; but it amounts to the same thing, because Appscript::Application
is a subclass of Appscript::Reference
).
Here’s what’s really going on. We are communicating with a scriptable application. That scriptable application is populated by a world of objects. For example, in the iTunes world, there are playlists and songs. In the Finder world, there are files and folders. When we say get
to a scriptable application, the scriptable application cannot actually send back to us any of its own objects. It can’t even send a pointer to any of its own objects. The two worlds — our world, and the world of the scriptable application — are separate and are not synchronized with each other.
So the scriptable application uses two strategies to send us a value:
If a value is “simple”, such as a string or a number, the scriptable application makes a copy of the value and sends us the copy. So, for example, if we ask for the value of the name of the current track in iTunes, iTunes might tell us that it is "Broke and Hungry Blues"
. This is a string, but it is not the scriptable application’s own string; it’s a copy. If we mutate this string, altering it in place, we will have no effect on the name of the current track in iTunes. The two worlds are separate. We can change the name of the current track in iTunes, but not by mutating this string. (I’ll discuss how to change it in the next chapter.) Mutating the string merely changes our copy of the string.
(In a later chapter, I’ll discuss the various classes that Ruby uses to represent a value of this sort.)
If a value is one of the scriptable application’s “objects,” such as a folder or a playlist, the scriptable application sends us a reference to the object. A reference is a descriptive expression; in particular, it is an expression that we could use to specify that object in talking to the scriptable application. In reality, a reference is an object specifier, just like the object specifiers we examined when looking at raw Apple events in Chapter 3. In that chapter, do you remember how we asked iTunes for its selection, and we got back an Apple event that was a great big object specifier? That’s a reference.
Ruby (rb-appscript) packages a reference as an object of class Appscript::Reference
. This object can be used directly as an object specifier that we send to the scriptable application. But if we convert the object to a string, as we do with puts
, we are shown the Ruby expression that we would use to form this object specifier from scratch.
A reference that you form is itself a Ruby object. So you can form a reference now, assign it to a variable, and then use the reference later. You could use it by sending it a message that forms a further reference; or you could use it by sending it a command, such as get
, that causes an Apple event to be sent to the scriptable application. It’s important not to confuse those two uses of a reference, because you don’t want to send an Apple event unnecessarily.
Here’s an example of forming a reference, assigning it to a variable, and then using the reference later:
itu = Appscript.app("iTunes.app")
cur = itu.current_track # now cur is an Appscript::Reference
# ... time passes
puts cur.name.get #=> Broke and Hungry Blues
Only one Apple event was sent, in the last line; merely forming the reference, in the second line, doesn’t send an Apple event. Similarly, we could have done it this way:
itu = Appscript.app("iTunes.app")
curnam = itu.current_track.name # now curname is an Appscript::Reference
# ... time passes
puts curnam.get #=> Broke and Hungry Blues
Again, no Apple event was sent until the last line, when we said get
, because that’s a command.
What you should not do absent-mindedly is this:
itu = Appscript.app("iTunes.app")
cur = itu.current_track.get # we sent an Apple event
# ... time passes
puts cur.name.get #=> Broke and Hungry Blues
That code works — we did learn the name of the current track — but we sent two Apple events where one would have done. This is bad practice if done accidentally.
On the other hand, you might do it intentionally. If you want to capture a reference to the current track because you’re going to be interested later in what the name of the current track is now, then maybe you’d better use get
after all. Because, you see, objects in the scriptable application world can change. If you wait until later to find out about the current track, there might be a different current track. So if you want to talk later about what is now the current track, then you do want to get
the current track now and save in a variable the object reference that the scriptable application gives you. In fact, maybe you’d better get
the name right now! If you postpone getting the name, you might find that by the time you come to ask for it, your reference is useless because the track has been deleted! (Oh, it’s a slippery world, the world of a scriptable application.)
class_
PropertyWe have seen that, in our Ruby world, a value that we obtain from a scriptable application with get
might be something like a String, or a Symbol, or an Appscript::Reference
. That is its Ruby class. But in the scriptable application’s world, things have classes too. It can often be useful to know what class the scriptable application thinks a value is. To distinguish between the Ruby notion of class and the scriptable application’s notion, we’ll use the term class_
to refer to the latter.
A class_
is not quite the same as the Ruby notion of a class. In the scriptable application’s world, there are not really classes and instances the way there are in a truly object-oriented medium like Ruby. In fact, class_
is merely a property of an object in the scriptable application’s world; in a way, it’s just a name. However, it’s a bit more than this: it’s a name plus a promise. The promise is that if two values have the same class_
, they have the same attributes. (Not the same attribute values; the same attribute names.) Properties are attributes, so this means they have the same properties. (Not the same property values; the same property names.)
So, if you know the properties of a class_
, and if you have a reference to an object of that class_
, you now know what that object’s properties are. That’s very important, because it tells you some things you can do with that object. For example:
itu = Appscript.app("iTunes.app")
p itu.current_track.class_.get #=> :file_track
So, the current track in iTunes is at this moment a :file_track
object; its class_
is :file_track
.
Observe that the class_
name is reported to us a symbol. This is why I refer in this chapter to “the :application
object”; what I mean is, the scriptable application’s single object whose class_
is reported as :application
.
Now, I happen to know something about :file_track
objects: I know, for example, that a :file_track
object has a name
property. Let’s prove it:
itu = Appscript.app("iTunes.app")
puts itu.current_track.name.get #=> Broke and Hungry Blues
It worked! I was right. Of course, I’ve already done this several times already in this chapter, so you’re not surprised that it works. But you should be asking yourself: how did I know that a :file_track
object has a name
property? The iTunes dictionary told me so. (Remember, I said in Chapter 3 that the dictionary was human-readable. This is the kind of information that the dictionary is good at providing to human beings. I’ll discuss the dictionary in detail in a later chapter.)
Another similarity between a class_
and a Ruby class is that one class_
can inherit from another. For example, the current_track
need not be a :file_track
; it might be a :URL_track
, and there are other possibilities as well. But all such possibilities have certain things in common, and this commonality is expressed by the fact that these classes all inherit from the :track
class. The current_track
property, if it has a value, will be a :track
, meaning a :track
or some subclass thereof.
The notion of inheritance here is extremely simple-minded; what’s inherited is, in fact, exactly the list of properties. For example, a :file_track
has a name
property because it inherits this fact from :track
. But there is also a difference between a :file_track
and a :track
(if there weren’t, they would be effectively the same class_
). A :file_track
has a location
property (pointing to the file); a :track
doesn’t. A :URL_track
, on the other hand, instead of a location
property, has an address
property (the URL).
Figure 5–1 uses these facts to suggest the relationship between class_
, properties, and inheritance.
Figure 5–1
Unfortunately, the scriptable application has very limited introspective ability; so, unlike Ruby, you can’t quiz an object in the scriptable application’s world as to its inheritance. You can ask the current_track
for its class_
and learn that it is a :file_track
, but you cannot then ask it for its superclass and learn that this is :track
; you have to know in some other way that :file_track
inherits from :track
. That other way is, of course, the dictionary.
The definition of a class_
whose objects are represented to us as a simple value, such as a string, is basically universal across all scriptable applications. But in the case of a class_
whose objects are represented to us as an Appscript::Reference
, every scriptable application is free to define differently what properties objects of that class_
will have (and how that class_
fits into the inheritance structure). For example, :item
is a class_
in iTunes, the Finder, and BBEdit, but it’s different in each. This is just one of the many things that makes scripting Mac applications tricky.
An element is an object attribute that is an object reference specified in terms of its class_
. Think of the object as being endowed with arrays of object references, where all the objects referred to in each array are of the same class_
(or subclasses of the same class_
). Then we can use the name of the class_
to refer to such an array; actually, in rb-appscript, we use a method whose name is the plural of the class_
.
So, for example, the iTunes :application
object has :playlist
elements. This means we can think of the iTunes :application
object as having an array of :playlist
object references, and we can refer to this array by sending the iTunes :application
object the playlists
message.
itu = Appscript.app("itunes.app")
p itu.playlists.get
The output here is an array of object references; I’ll just show the start of the array (and the results on your machine will, of course, be somewhat different):
[app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231),
app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(399),
app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(444),
...]
In iTunes, :library_playlist
is a subclass of :playlist
, and :user_playlist
is another subclass of :playlist
. So the iTunes :application
object is holding on to a bunch of references to objects that are :playlist
objects (meaning :playlist
and its subclasses), so when we ask for its playlists
, we’re talking about the array of those references. Not surprisingly, we could also obtain some of those very same references as part of a different array, by using a subclass name plural, such as library_playlists
:
itu = Appscript.app("itunes.app")
p itu.library_playlists.get
#=> [app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231)]
That was a much shorter array, consisting of just one item — a reference to the very same playlist that was the first item of the playlists
array.
As with properties, so with elements, the way we know what kinds of element an object has is chiefly because we know something about the object’s class_
. (And we know it by consulting the dictionary.) But of course we don’t know beforehand exactly what elements of each kind an object has; to find that out, we have to ask the scriptable application. The answer depends on the situation in the scriptable application at the moment. My copy of iTunes has the particular playlists it has because those are playlists I’ve given it; your copy has a different set of playlists (and my copy could have a different set of playlists a few minutes from now, because I can create or delete a playlist).
The relationship between an object and its elements is often thought of as “has” or “contains”: iTunes (the application) “has” playlists, a playlist “contains” tracks. But perhaps it would be better expressed as “might have” or “can contain”, since at a given moment an object’s array of elements for a certain class_
might be empty. A playlist might contain no tracks, for example.
We’ve just seen that the iTunes :application
object has many :playlist
elements, and that we can get an array of references to these with the playlists
method. But how can we refer to a particular :playlist
element? To do so, we need to form an element specifier. There are actually eight different forms of element specifier.
The method referring to an entire element array is the plural of the class_
name. To obtain this array as a Ruby array of references to all the elements, send it the get
message. That’s what we did in the examples in the previous section:
itu = Appscript.app("itunes.app")
p itu.library_playlists.get
#=> [app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231)]
The items in an element array are numbered sequentially, starting at 1. (Rubyists, take note: in the scriptable application world, numbering starts with 1, not 0.) This number is called the item’s index. The syntax for referring to an element by index is elements[n]
, where what’s in brackets is an integer. So, for example:
itu = Appscript.app("itunes.app")
p itu.playlists[1].get
#=> app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231)
Okay, now pretend I’m jumping up and down, waving my arms, and screaming: the [1]
in that example is not the Ruby Array item accessor. The playlists
method doesn’t return an array; it returns a reference (an Appscript::Reference
). The above example doesn’t fetch the entire array of playlists and then get item 1 of that array; it asks the iTunes :application
object for just one playlist, namely, the first playlist in its (internal) array. And that’s a very good thing. It can be time-consuming and foolish to ask a scriptable application for an entire array of references when all you want is one particular reference.
If we did want to fetch the entire array, we would use get
on the element array itself (the previous type of element specifier). Then we would have a Ruby array, and its first item is numbered 0:
itu = Appscript.app("itunes.app")
p itu.playlists.get[0]
#=> app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231)
Be very, very sure you understand the difference between that example and the previous one. They do very different things, even though the output is the same.
As in Ruby, the last item in an element array can be referred to with index -1
, the next-to-last item can be referred to with index -2
, and so on. Could Ruby have adopted this feature from AppleScript? (I don’t think so.)
The index of an object can change in real time. For example, among windows, windows[1]
is usually the frontmost window. If you bring a window to the front (or if the user does), its index may change. Similarly, among the files in a folder in the Finder, index order is alphabetical display order; if you change a file’s name (or if the user does), its index can change. This fact is the basis of many bugs in beginners’ scripts.
Objects in an element array usually have names. The syntax for referring to an element by name is elements["name"]
, where what’s in brackets is a string. So, for example:
itu = Appscript.app("itunes.app")
p itu.playlists["blindlemon"].get
#=> app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(526)
Objects nearly always have a name
property, whose value is the same as the name used to specify the object as an element:
itu = Appscript.app("itunes.app")
p itu.playlists["blindlemon"].name.get #=> blindlemon
Unlike Ruby, string comparison in the scriptable application world is generally not case sensitive, so the case of the name string shouldn’t matter.
Objects in an element array may have a unique identifier called an ID, which is usually a number. When they do, the syntax for referring to an element by ID is elements.ID(n)
, where the parameter of the ID method is the ID value. For example, we already know that on my machine the “blindlemon” playlist has ID value 526:
itu = Appscript.app("itunes.app")
p itu.playlists.ID(526).get
#=> app("/Applications/iTunes.app").sources.ID(41).user_playlists.ID(526)
And just in case you’re in doubt that it’s the same playlist, we can ask for its name
:
itu = Appscript.app("itunes.app")
puts itu.playlists.ID(526).name.get #=> blindlemon
Objects that can be specified by ID will typically have an id_
property that lets you learn the object’s ID value:
itu = Appscript.app("itunes.app")
puts itu.playlists["blindlemon"].id_.get #=> 526
An object’s ID does not change in real time, the way its name and index can; therein lies much of its value to the programmer. (But an object’s ID might be different if you quit the scriptable application, start it up again, and obtain a reference to the “same” object. For example, if I quit iTunes today, then the “blindlemon” playlist might not have ID value 526 when I start up iTunes tomorrow.)
There are a few named positions within the element array that can be used to specify an element:
elements.first
— the first one, same as elements[1]
elements.last
— the last one, same as elements[-1]
elements.any
— a randomly selected element
elements.middle
— the middle one, whatever that may mean (rarely used)
Sometimes an array of elements positioned contiguously within an element array may be specified by giving specifiers for the first and last, separated by comma, like this: elements[m,n]
. The values in square brackets are typically index numbers, but some applications are more generous and allow you to use a name. Some scriptable applications are averse to this mode of element specification, and iTunes seems to be one of them, so I’ll use the Finder instead:
f = Appscript.app("Finder.app")
p f.files[1,2].get
And here’s the result:
[app("/System/Library/CoreServices/Finder.app").desktop.document_files["aha.rtf"],
app("/System/Library/CoreServices/Finder.app").desktop.document_files["ahoy.rtf"]]
The Finder is one of those generous applications that let us use names instead of index values; here, I’ll use a number and a name:
f = Appscript.app("Finder.app")
p f.files[1,"ahoy.rtf"].get # (same result as before)
(There is another way of getting elements by range, where the range is marked off by element specifiers of a class_
that isn’t the element class_
we’re after. But it is so rarely implemented by scriptable applications that I’m not going to talk about it.)
It may be possible to specify an element in terms of its preceding or succeeding something else. The syntax here is to chain to an object reference the next
or previous
method, which takes the name of a class_
(a symbol) as its parameter.
If what an element precedes or succeeds is another element of the same class_
, and if this is specified by index, this mode of element specification is all but superfluous, since we need only increment or decrement the index:
f = Appscript.app("Finder")
puts f.files[1].next(:file).get
#=> app("../Finder.app").desktop.document_files["ahoy.rtf"]
puts f.files[2].get
#=> app("../Finder.app").desktop.document_files["ahoy.rtf"]
However, element specification by relative position starts to look a bit more interesting when the initial object is specified in some other way, such as by name:
f = Appscript.app("Finder")
puts f.files["aha.rtf"].next(:file).get
#=> ...document_files["ahoy.rtf"]
Or, the initial object might be specified as a property:
f = Appscript.app("finder")
puts f.home.next(:folder).get
#=> app("...Finder.app").startup_disk.folders["Users"].folders["otheruser"]
Things really start to look interesting when the initial object and the element we’re after have a different class_
from one another:
f = Appscript.app("Finder")
puts f.folders[1].next(:file).get
#=> ...document_files["aha.rtf"]
What’s going on in that example? In the Finder, on the desktop, I’ve got both folders and files, interspersed. I have a folder called “aaargh”. This precedes the file “aha.rtf”, because things in the Finder have a natural order, alphabetical order, even when they are of different types. So “aha.rtf” is the first file after the first folder. (It’s rather like the way you’d specify Easter if the calendar were a scriptable application: vernal_equinox.next(:full_moon).next(:sunday)
.)
An application that implements relative element specifications in a particularly useful way is BBEdit. In BBEdit, every text-element class (such as :character
and :word
) has :insertion_point
elements, which are the positions between the characters. Relative position is a very good way to specify an :insertion_point
, which you can then use to insert text at that point. For example, suppose the frontmost BBEdit document starts with “This is a test.” Then:
bb = Appscript.app("BBEdit.app")
bb.documents[1].words[4].previous(:insertion_point).contents.set("great ")
Now the document starts with “This is a great test.” (We haven’t come to the set
command yet, but what a great test.)
We come at last to one of the most powerful and interesting ways of specifying elements. It may be possible to describe the desired element(s) in terms of a boolean test — that is, those elements of which a certain condition is true.
To see why this is powerful, let’s consider such a test. In iTunes there are tracks, and a track can have an artist. Imagine that we want the tracks whose artist is Blind Lemon Jefferson. Now, obviously we could just interrogate all the tracks in succession: “How many tracks are there? 11437? Fine. Tell me the artist of track 1. (We look to see if it’s Blind Lemon Jefferson.) Now tell me the artist of track 2. (We look to see if it’s Blind Lemon Jefferson.) Now tell me the artist of track 3…” This is tremendously inefficient on a number of grounds, not least the huge number of Apple events being sent. With a boolean test specifier, we send just one Apple event describing the test, and iTunes performs that test, internally, picking out just the requested elements for us.
Clearly the power of this approach has its limits. The target application must implement the test we request, and the test must be within the power of Apple events to express. It is not hard to bang up against these limits. For example, if our test would involve matching a string against a regular expression, we’re out of luck, because Apple events (and most scriptable applications) know nothing of regular expressions; so in that case, we really might need another approach (and I’ll show such an approach in examples later in this book). But Apple events can express a surprisingly wide range of conditions, and a boolean test specifier can be an elegant time-saver.
The syntax of a boolean test is elements[condition]
, where what’s in brackets has two essential parts:
A boolean test is performed either against each element as a whole or against a property of each element. (For example, we want the tracks whose artist
property is Blind Lemon Jefferson.) Either way, the condition must start with a reference that stands in for the notion “each element”. That reference is the Appscript.its
method. The condition will thus start with Appscript.its
or Appscript.its.property_name
.
The test itself is a method sent to the reference that starts the condition. The method takes one parameter — the other comparand. The test methods are:
eq # equals
ne # does not equal
lt # is less than
le # is less than or equal to
gt # is greater than
ge # is greater than or equal to
begins_with
does_not_begin_with
ends_with
does_not_end_with
contains
does_not_contain
is_in
is_not_in
Let’s demonstrate by performing our test for tracks whose artist is Blind Lemon Jefferson. The tracks must be specified as elements of some playlist; I’ll use the Library playlist, thus effectively testing against all tracks.
itu = Appscript.app("iTunes.app")
puts itu.playlists["Library"].tracks[Appscript.its.artist.eq("Blind Lemon Jefferson")].get
The result is an array of references to five tracks.
Having to say Appscript.its
at the start of every boolean test gets old fast. One solution is to include Appscript
, but I don’t like sullying the global namespace unnecessarily, so I prefer to define a synonym if I’m going to be doing any boolean tests. Here’s a rewrite of that same code; it takes more lines, but the boolean test itself is a lot more compact and readable:
itu = Appscript.app("iTunes.app")
whose = Appscript.its
blj = "Blind Lemon Jefferson"
puts itu.playlists["Library"].tracks[whose.artist.eq(blj)].get
Optionally, a boolean test may be extended by any of the boolean operators and
, or
, and not
. These are methods sent to the test as a whole. The first two, and
and or
, take as parameter one or more additional tests. These tests must be complete; that is, they themselves must effectively start with Appscript.its
. Here, I’ll ask for tracks whose artist is Blind Lemon Jefferson and whose title (name) contains “Blues”:
itu = Appscript.app("iTunes.app")
whose = Appscript.its
blj = "Blind Lemon Jefferson"
lib = itu.playlists["Library"]
puts lib.tracks[whose.artist.eq(blj).and(whose.name.contains("Blues"))].get
The result is an array of references to three tracks (Blind Lemon Jefferson did have a propensity for songs with “Blues” in their title).
As with name specifiers, so too string comparisons in a boolean test are generally not case-sensitive.
Scriptable applications will often allow an operation to be distributed over multiple references that have been gathered internally. A typical such operation is referring to a property. So, suppose you use an element specifier that can result in multiple references. And suppose what you really want to talk about is not those references, but some property of each of those references. In many cases, you can simply send the method naming that property directly to the element specifier.
So let’s say what I really want to know is the name of every Blind Lemon Jefferson song whose title contains “Blues”:
itu = Appscript.app("iTunes.app")
whose = Appscript.its
blj = "Blind Lemon Jefferson"
lib = itu.playlists["Library"]
p lib.tracks[whose.artist.eq(blj).and(whose.name.contains("Blues"))].name.get
#=> ["Match Box Blues", "Broke and Hungry Blues", "Hangman's Blues"]
It will be readily seen that this is extremely cool and convenient. We’ve saved time and we’ve reduced a potential four Apple events to just one. A common technique is to ask for a list of things that can be used later to form element specifiers. For example:
itu = Appscript.app("iTunes.app")
playlist_names = itu.playlists.name.get
Now I have an array of the names of all the playlists. If those names are unique, I can use them later to do something with each playlist, using each name to form a playlists
element specifier.
Another common technique is to fetch values for more than one property and associate them. The association works because the underlying array of multiple internal references is the same array, so the properties arrive in the same order. Forming such an association is particularly easy in Ruby, thanks to its zip
Array method:
itu = Appscript.app("iTunes.app")
trax = itu.playlists["Library"].tracks
arr = trax.name.get.zip(trax.artist.get)
After that, arr
is something like this:
[["Recuerdos de la Alhambra Tarrega", "Andres Segovia"],
["Match Box Blues", "Blind Lemon Jefferson"],
["Broke and Hungry Blues", "Blind Lemon Jefferson"],
...]
In some cases you may be able to refer to elements of multiple references. For example:
itu = Appscript.app("iTunes.app")
p itu.playlists.tracks.get
The result is an array of arrays, each inner array consisting of references to each of the tracks of one playlist. That’s a tremendous amount of organized information resulting from a single Apple event. The possibilities boggle the mind.
(Also, in some cases, multiple internal references can be used as a command parameter; we’ll see examples in a later chapter.)
We have now basically discussed all the ways of referring to an object that a scriptable application is willing to dispense. Furthermore, we’ve seen that all references begin ultimately with the :application
object (represented by an Appscript::Application
instance). So if there’s an object in a scriptable application’s world, and if the scriptable application wants to make this object available to scripters, the scriptable application had better make it possible to reach this object via a chain of references starting at the :application
object.
Since elements describe a potential one-to-many containership relationship, we can loosely imagine successive containers and their elements as arranged in a tree structure. The chain of references reaches a desired object by traversing this tree. This tree arrangement of the application’s objects is called its object model.
Figure 5–2 shows a tiny part of the object model for one user’s copy of iTunes. The diagram shows elements as class_
names followed by specifiers for some particular elements. In real life, each iTunes class_
has many more elements, and the tree is much more elaborate.
Figure 5–2
In Figure 5–2, one :application
property, current_track
, is shown in order to point out that although the object model is primarily about elements, a property can provide an alternate way of accessing an object. In iTunes, another example is that the :application
object has :browser_window
elements, representing the primary player window; a browers window’s view
property is the playlist currently displayed in the window, so this is a completely different way to access a playlist.
Also, some objects can be referred to only by way of a property; for example, the Finder :application
object has a Finder_preferences
property whose value is the application’s single :preferences
object; no object in the Finder’s object model has :preferences
as an element.
Different elements, too, can provide alternate paths through the tree to refer to an object. We’ve already seen, for example, that a :playlist
has :file_track
elements, :file_track
being a subclass of :track
; in Figure 5–2, both Beethoven tracks are in fact :file_track
s, so we could have used playlists["Beethoven"].file_tracks
to refer to them.
Sometimes an application will permit you to omit part of the chain of elements along the path towards specifying an object. Actually, I’ve been using this feature throughout this chapter, and you may have been wondering about it. For example, in iTunes, we can say:
itu = Appscript.app("iTunes.app")
puts itu.playlists["library"].tracks[1].get
#=> app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231).file_tracks.ID(261)
If you look in the iTunes dictionary, the :application
object has no :playlist
elements. Yet we can successfully say itu.playlists
, with no intermediate sources
element. The reason is that iTunes permits us to use a shortcut; when we say itu.playlists
, iTunes assumes that we mean itu.sources["library"].playlists
. This makes perfect sense, because the other :source
element, itu.sources["radio"]
, has no playlists! So this is a reasonable convenience for iTunes to offer us. The fact that it is undocumented in the dictionary, however, is annoying; the shortcut can be discovered only by experimentation.
Similarly, this works in the Finder:
f = Appscript.app("Finder.app")
puts f.files[1].get
#=> app("/System/Library/CoreServices/Finder.app").desktop.document_files["aha.rtf"]
The :application
object in the Finder dictionary does have :file
elements, but it remains an undocumented mystery what this could possibly mean; files can only live in a folder, or at the top level of a volume (a disk). Yet we can say f.files
anyway, and when we do, we discover that the Finder assumes we mean “on the desktop” (expressed here through the :application
object’s desktop
property). Again, this is a lovely convenience, but some documentation would have been nice. You can expect to encounter similar shortcuts across the object model in many other scriptable applications as you experiment.
A different sort of shortcut sometimes occurs when an element specifier points to a stretch of text (in a word-processing application, for instance). Consider this example of scripting TextEdit:
te = Appscript.app("TextEdit.app")
puts te.documents[1].words[4].get #=> test
TextEdit didn’t return an object reference at all; it extracted some actual text and returned it. There is furious debate in the scripting community as to whether this is proper behavior; personally, I think that it is not (and that TextEdit’s object model and behavior for referring to text is just about the worst in the universe). Contrast BBEdit:
bb = Appscript.app("BBEdit.app")
puts bb.documents[1].words[4].get
#=> app("...BBEdit.app").text_documents.first.characters[11, 14]
That, at least is a true reference. To obtain the text itself requires a further step:
bb = Appscript.app("BBEdit.app")
puts bb.documents[1].words[4].contents.get #=> test
In a sense, then, TextEdit is shortcutting from a stretch of text to the text itself.
It can hardly have escaped your attention that in examples throughout this chapter, when a scriptable application is asked for a reference, it often provides it in a form quite different from that of the request. Here’s an example:
itu = Appscript.app("iTunes.app")
puts itu.playlists["library"].tracks[1].get
#=> app("/Applications/iTunes.app").sources.ID(41).library_playlists.ID(231).file_tracks.ID(261)
We specified a playlist by name and a track by number, and omitted the sources
step altogether; what we got back included the sources
step, the playlists
and tracks
have been resolved to the actual subclasses of the objects specified, and all element specifiers are by ID. Here’s another example:
f = Appscript.app("Finder.app")
puts f.files[1].get
#=> app("/System/Library/CoreServices/Finder.app").desktop.document_files["aha.rtf"]
We omitted the desktop
step, and specified our file by index; what we got back includes the desktop
step, files
has been resolved to the actual subclass of the object specified, and the element specifier is by name. Here’s an even more extreme example in the Finder:
f = Appscript.app("Finder")
puts f.files["aha.rtf"].next(:file).get
#=> app("...Finder.app").startup_disk.folders["Users"].folders["mattneub"].folders["Desktop"].document_files["ahoy.rtf"]
Now the Finder starts all the way at the top of the folder hierarchy, with the startup_disk
property, and works its way down the folder hierarchy with name specifiers to reach the object in question.
There’s no particular lesson here (and, as we’ve just seen, no particular consistency). When we get an object reference from a scriptable application, it can couch that reference in any form it likes. It certainly need not be the same as the form we supplied to start with, so we shouldn’t be surprised. What we can assume is that what the scriptable application gives us is a canonical reference, that is, a reference that we could reliably use to specify the same object. In iTunes, for example, two tracks in a playlist might have the same title, which is the track’s name
property; so iTunes uses an ID specifier instead. In the Finder, however, two items in the same folder cannot have the same name, so the Finder uses name specifiers.
To a Ruby programmer, exceptions are exceptional. But in the world of scriptable applications, it is quite common to see an exception raised over the most trivial offense. Such an exception, like any Ruby exception, must be handled if it is not to percolate up to top level and cause your program to terminate.
A good example is the iTunes :application
object’s current_track
property. If you ask for this property’s value and there is no current track, iTunes responds with an exception.
itu = Appscript.app("iTunes.app")
puts itu.current_track.get
#=> Appscript::CommandError: CommandError OSERROR: -1731 MESSAGE: Unknown object type.
#=> Appscript::CommandError: CommandError OSERROR: -1728 MESSAGE: Can't get reference.
I display two different possible error messages, because different versions of iTunes raise different exceptions under these circumstances. But the important thing is that these are valid error messages, being passed on to you by rb-appscript from iTunes itself (the Appscript::CommandError
class designation tells you so), and that iTunes evidently regards raising an exception as a perfectly reasonable way of saying “There is no current track”. A Rubyist would expect a nil
result; the world of scripting applications does have something parallel, :null
, but I have never once seen it used in all my years of scripting. Another alternative is :missing_value
, and mercifully, scriptable applications are increasingly using this; but exceptions remain common.
The takeaway message is that you must be prepared for exceptions. In iTunes, the mere query as to whether there is a current track in iTunes can cause an exception. So your program must be constructed accordingly; don’t even mention the current track without supplying exception-handling. Here’s a sound bit of programming for reporting the name of the current track.
itu = Appscript.app("iTunes.app")
begin
name = itu.current_track.name.get
rescue Appscript::CommandError
puts "There is no current track."
else
puts name
end
Let’s say that part of a reference is not known until your script runs. For example, while scripting the Finder, you might want a list of all the files in a folder or all the folders in that folder, but the script itself is to decide which:
f = Appscript.app("Finder")
which = "files" # actual logic of choice omitted here
case which
when "files"
p f.folders[1].files.name.get
when "folders"
p f.folders[1].folders.name.get
end
This seems wasteful and error-prone. The case
statement distinguishes two references that are nearly identical:
f.folders[1].which.name.get # pseudo-code!
The only thing that differs is which
. It would be far more elegant somehow to set which
to "files"
or "folders"
and resolve it at runtime:
f = Appscript.app("Finder")
which = "files" # actual logic of choice omitted here
p f.folders[1].which.name.get # pseudo-code!
That isn’t real code, but how can we make it real? One way is to take advantage of Ruby’s send
method, which takes a string or symbol denoting the method that is to be called. This works because in rb-appscript, everything you say as you construct a reference is a method. So:
which = "folders" # actual logic of choice omitted here
p f.folders[1].send(which).name.get
However, this breaks down if the dynamically formed partial reference consists of more than a single method. For example:
f = Appscript.app("Finder")
which = "files.name" # actual logic of choice omitted here
p f.folders[1].which.get # pseudo-code!
How can we implement something like that? Poking around in rb-appscript’s source code, I think I’ve come up with a way, using some lower-level AEM
methods. You’ll need to add a method to Appscript::Reference
, as follows:
class Appscript::Reference
def append(which)
collector = AEMReference::CollectComparable.new
which.AS_aem_reference.AEM_resolve(collector)
aemref = self.AS_aem_reference
collector.result[1..-1].each do |command, args|
aemref = aemref.send(command, args)
end
Appscript::Reference.new(self.AS_app_data, aemref)
end
end
And here’s how to use it:
f = Appscript.app("Finder")
refbase = f.folders[1]
refpartial = f.files.name # must say "files.name" to something
ref = refbase.append(refpartial) # f.folders[1].files.name
The third line contains a necessary trick. As the comment points out, we must form our second, partial reference in the same way as we would form any Appscript::Reference
: we must start with an Appscript::Reference
or Appscript::Application
instance and send method calls to it. Therefore we start with our reference to the Finder, where rb-appscript will permit us to use legal Finder terms like files
. The append
method strips out that Finder reference (that’s what result[1..-1]
does) before appending the second reference to the first.
The result of append
is a new Appscript::Reference
instance, and you can now proceed to send it further methods:
p ref.get #=> ["index.html"]
In a very small number of cases (only one is known, actually) an application may return a canonical reference that it cannot itself accept later on:
em = Appscript.app("Expression Media")
w = em.windows[1].get
itms = w.media_items.get
#=> Appscript::CommandError: CommandError OSERROR: -1703 MESSAGE: Some data was the wrong type
The symptom is that we get
an object reference and then later cannot reuse it. The solution is to switch off rb-appscript’s internal object caching, like this:
em = Appscript.app("Expression Media")
em.AS_app_data.dont_cache_unpacked_specifiers # switch off object caching
w = em.windows[1].get
itms = w.media_items.get # works fine
What’s happening here is that, behind the scenes, instead of sending back to Expression Media the very same object reference that it sent us, rb-appscript is deconstructing the object reference and reconstructing it in a valid form. This takes a little longer but it fixes the problem.
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!