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 4: The Application Object
2. Getting an Application Object
2.1. Using app.by_name
2.2. Using app.by_id
2.3. Using app.by_creator
2.4. Using app.by_pid
2.5. Using app.by_url
3. Other Application Object Factory Methods
4. Checking Whether an Application is Running
All activity in Ruby involves sending a message to an object, and all application scripting involves sending an Apple event to a scriptable application. Small wonder, then, that the first thing you need in order to script an application with Ruby is an object representing the target application. This chapter describes how to get one.
Getting an application object may seem like a small thing, but it’s crucial. The longest journey starts with a single step, and without this single step, there’s going to be no journey at all. Scripting an application is like talking to a friend over the phone; getting an application object is like dialing the phone and having your friend answer. You can’t talk to your friend at all until the call has gotten through and you know your friend is waiting there at the other end. So let’s dial that phone!
In rb-appscript, the high-level way to get an application object is by way of the Appscript
module. The Appscript
module lives in the file appscript.rb. So before any Ruby program can target a scriptable application, it is necessary to make sure that appscript.rb is loaded.
How you load appscript.rb depends on how you’ve installed rb-appscript. I can’t know how you’ve done this, so I can’t give precise instructions for your situation. However, I’ll just describe a couple of the most likely scenarios.
If you have installed rb-appscript in a directory listed in $:
, you can directly require appscript.rb:
require 'appscript'
If you have installed rb-appscript as a gem (sudo gem install rb-appscript
at the command line), then in theory it should be sufficient to require appscript.rb:
require 'appscript'
In real life, however, older versions of Ruby sometimes need rubygems.rb to be loaded before you can load a gem with require
:
require 'rubygems'
require 'appscript'
To test whether your way of loading appscript.rb is succeeding (though you should not be in any doubt of this, since you would get an error if you tried to load it and failed), you can look to see whether the Appscript
module exists:
puts defined?(Appscript) #=> constant, meaning it exists
In this book, nearly every example script simply assumes that you have already loaded the Appscript
module; it would be very boring if I said require 'appscript'
at the start of every script, so I’ll usually just omit it.
A scriptable application to which you’re going to send Apple events is represented in rb-appscript by an object of class Appscript::Application
. The way to obtain one, however, is not by calling new
on the Appscript::Application
class — indeed, to prevent you from doing so accidentally, the new
method of the Appscript::Application
class is private. Rather, you use a factory method, Appscript.app
. This calls Appscript::GenericApplication.new
for you, cleverly generating an Appscript::Application
instance, and now you are expected to specify what scriptable application you want to talk to. There are five commonly used methods for doing this; which one you use depends upon how you want to specify the desired application:
by_name
by_id
by_creator
by_pid
by_url
Let’s take each of them in turn.
app.by_name
Use app.by_name
when you want to specify an application by its name or by its pathname. You supply a string (expected to be in UTF-8 encoding); if the string starts with a slash ("/") it is taken to be a pathname. If you know that the application’s name ends in .app you can save some time by including the extension at the end of the name or pathname; but this is not required (if the attempt to find the application in the form you gave it fails, rb-appscript will append ".app"
and try again), and if you are in doubt, it might be best not to add it, since some application names do not end in .app.
Appscript.app.by_name("iTunes")
If you supply just a name, rb-appscript calls the Launch Services function LSFindApplicationForInfo
to see if it can provide the pathname. AppleScript users should note that this is not the same as what AppleScript does when you identify an application by name; for example, with rb-appscript you can’t specify Microsoft Excel by its short name, “Excel”, even if it is running, the way you can in some versions of AppleScript. (By the way, LSFindApplicationForInfo
is not case-sensitive.)
Using the name has the advantage over using the pathname that you don’t need to know where the application is installed (which, on any machine but your own, you probably would not know). However, you may prefer to use the pathname if you do know where the application is installed, and if you are concerned about either of the following things:
Speed. Specifying a pathname is considerably faster, because rb-appscript doesn’t have to call LSFindApplicationForInfo
— it just looks to see if a file actually exists at the pathname you specified.
Versions. If you specify an application by name alone, then if there are multiple applications by that name, LSFindApplicationForInfo
might pick the wrong one.
It is so common to use app.by_name
that rb-appscript permits you to treat app
alone as an equivalent to it. Thus, a typical way to form an Appscript::Application
instance by name is like this:
Appscript.app("iTunes.app") # adding ".app" makes it faster
app.by_id
Use app.by_id
when you want to specify an application by its bundle identifier. The bundle identifier is a string unique to the application; by convention, and to help increase the likelihood of uniqueness, it is formed using “reverse DNS notation”, often starting with "com"
and including the company or developer name, like this: "com.neuburg.zotz"
. Every application is supposed to have a bundle identifier, and although in practice some older legacy applications may not have one, the bundle identifier is generally the best way to identify an application, since the user can change an application’s name in the Finder, but the application’s bundle identifier is unlikely to be changed, and works regardless of where the application is installed. Like app.by_name
when a mere name is specified, app.by_id
calls LSFindApplicationForInfo
to get the application’s pathname.
Unfortunately (and ironically), learning an application’s bundle identifier is extraordinarily difficult. One of the simplest ways is (even more ironically) to use an AppleScript scripting addition command, info_for
. This lets you start with an application’s pathname and obtain its bundle identifier. Here, for example, is how to learn the iTunes bundle identifier using Ruby to call info_for
:
require 'osax' # might have to require 'rubygems' first
puts OSAX.osax.info_for(
MacTypes::Alias.path("/Applications/iTunes.app"))[:bundle_identifier]
#=> com.apple.iTunes
(I won’t be explaining about how rb-appscript calls scripting addition commands until a later chapter, so for now you should just take my word for it and slavishly copy the above example.)
Now that we know the iTunes bundle identifier, we can use it to specify iTunes and get an Appscript::Application
instance:
Appscript.app.by_id("com.apple.iTunes")
app.by_creator
Use app.by_creator
when you want to specify an application by its four-letter creator code. The creator code is the legacy way of specifying an application uniquely, so it should work for older applications where app.by_id
will not. Newer applications may have a creator code, but it isn’t required, so many do not. Moreover, some applications use the creator code incorrectly (a notorious example is that Adobe Reader and Adobe Acrobat share the same creator code). So it is probably advisable not to use the creator code as an identifier unless you have to (because there’s no bundle identifier). Like app.by_id
, app.by_creator
calls LSFindApplicationForInfo
to get the application’s pathname.
Since we already used info_for
to learn the iTunes bundle identifier, we may as well use it to learn the iTunes creator code as well:
require 'osax' # might have to require 'rubygems' first
puts OSAX.osax.info_for(
MacTypes::Alias.path("/Applications/iTunes.app"))[:file_creator]
#=> hook
And now we can use it to specify iTunes and get an Appscript::Application
instance:
Appscript.app.by_creator("hook")
app.by_pid
Use app.by_pid
when you want to specify a running application by its process id. The process id is a number assigned by Unix (which, as you know, underlies Mac OS X) as a process starts running. Every running process at any given moment has a unique process id, which it retains until it exits, so this is a way of uniquely identifying this process among all the processes running at this moment.
Obviously, this approach works only if the desired application is already running; but it has the advantage, in such a case, that it’s extremely efficient, and of course it can distinguish between multiple versions of the same application, because even if multiple versions of the same application are running, they have different process ids.
Here are two ways to obtain an application’s process id. First, the Unix way, at the command line. We’ll use the ps
command to get a list of all running processes and their process ids, and grep
to eliminate all running processes whose names don’t include the name of our process:
$ ps -cx | grep "iTunes"
1522 ?? S 0:02.88 iTunes
So the iTunes process id at this moment is 1522, and we could use this to target iTunes, as long as iTunes continues running.
Another way to obtain an application’s process id is by scripting System Events, a scriptable utility application that comes with Mac OS X. If we tell System Events the name of a running process, it will tell us its process id, which it calls the unix_id
. So now we’re back in Ruby:
puts Appscript.app("System Events.app").processes["iTunes"].unix_id.get #=> 1522
And now we can use this process id to specify the desired running process (iTunes) and get an Appscript::Application
instance:
Appscript.app.by_pid(1522)
If you want to make sure that a process id is going to work (that is, that there is such a process and that it is scriptable), you can take advantage of the is_running?
method, discussed below.
app.by_url
Use app.by_url
when you want to specify a running application on a different computer or running under a different user. It may come as a surprise to learn that this is possible; but it is. The reason is that there is a protocol, called eppc
, whereby an Apple event can be sent across the network. In order for this to work, Remote Apple Events must be turned on in the Sharing pane of System Preferences on the remote machine.
Here’s an example:
Appscript.app.by_url("eppc://hume.local/iTunes")
Note the structure of the URL. It is an eppc
URL; this is required. The URL hostname then identifies the computer; since this computer is on my local network (it’s in the living room, while I’m working at my office machine), I can use its Bonjour name to identify it. Finally, the URL path gives the name of the running process I’d like to talk to; in this case, it’s iTunes.
This is a URL, so if the process name, or any other part of the URL, is to contain a space, it must be represented by "%20"
, and so too with percent-escaping for any other illegal characters. Ruby includes a utility method, URI::escape
, to perform percent-escaping for you:
require 'uri'
puts URI.escape("My Cool App") #=> My%20Cool%20App
Merely forming an Appscript::Application
instance with app.by_url
, however, doesn’t tell you whether your URL is actually going to work or not. When rb-appscript forms an Appscript::Application
instance with the app.by_url
method, it merely records the URL; it doesn’t actually try to establish a connection to the remote application. To test properly, we should attempt to make the remote application do something. Anything will do, so let’s pick something minimal that all applications know how to do: launch
. This command has no effect on a running application, but it is sent to the running application, so it makes a good minimal test.
Appscript.app.by_url("eppc://hume.local/iTunes").launch
When I run that script, an authentication dialog (oddly ungrammatical, on my machine) appears asking for a username and password (Figure 4–1).
Figure 4–1
I provide the requested information, and the script runs to completion without error. This tells me that I’m forming the URL correctly.
(Note that the authentication dialog’s idea of what constitutes a valid username and password comes from the Remote Apple Events sharing settings on the remote machine. For example, if you’ve specified on the remote machine that Remote Apple Events are acceptable from any administrator, then this dialog is asking for the username and password of an administrator on the remote machine.)
Another, even more minimal way to make sure an eppc
URL is going to work is to take advantage of is_running?
, discussed below.
There are a number of variants on the format of the URL. First, I can avoid the authentication dialog requesting a username and password by supplying them as part of the URL, in the form username:password@
at the start of the hostname:
Appscript.app.by_url("eppc://mattleopard:teehee@hume.local/iTunes")
That has the disadvantage, of course, that the password is hard-coded into the script in plain text; it might be better to accept the dialog’s offer to record the password in the local machine’s keychain the first time we contact the remote machine.
Instead of using a Bonjour name, we can use an IP number. This works over the Internet, not just the local network; but this example just uses the same machine’s local IP number:
Appscript.app.by_url("eppc://192.168.15.102/iTunes")
Finally, it is possible to specify a user ID number, or a target process ID number (rather than the name of the target application process) if you know it; these appear as queries appended to the URL:
Appscript.app.by_url("eppc://192.168.15.102/iTunes?uid=501&pid=179")
One of the purposes of being able to specify a user ID number is that Mac OS X can have multiple users logged in simultaneously (“fast user switching”). Thus an application might be running under a user different from the user whose world is currently displayed on the screen. You can also use this technique to script an application running under a different user at the same computer where you’re already working. To do so, Remote Apple Events on this computer must be turned on. To learn a user’s uid, a simple approach is to use id
at the command line.
So, for example, I am user 501 (discovered by saying id
at the command line). I use fast user switching to log in as another user on this same machine; that user is 502 (discovered in the same way). Still in that user, I start up iTunes and select a song. Now I use fast user switching to switch back so that I’m in user 501 again. Now I’ll discover from here, in user 501, what song is selected in iTunes in user 502:
puts Appscript.app.by_url("eppc://localhost/iTunes?uid=502").selection.name.get
#=> "All Baroque Musick(1.FM TM)"
(Again, what the authentication dialog is looking for depends on your settings for Remote Apple Events in the Sharing Preferences Pane. If what is authorized is administrators, then the username and password being requested here are those of an authorized administrator account for this machine — which may well not be the username and password of user 502.)
An application must already be running in order to target it with app.by_url
. This raises the question of how one might launch a remote application in case it is not running. The best approach seems to be to use the Finder, which typically is running. Here’s an incantation that tells the Finder on a remote machine to launch iTunes, using the iTunes bundle identifier:
Appscript.app.by_url("eppc://192.168.15.102/Finder").application_files.ID("com.apple.iTunes").open
Now iTunes is running on the remote machine and can be scripted.
There are two further application object factory methods, but they are much less commonly used.
app.by_aem_app
uses an AEM::Application
instance to generate an Appscript::Application
instance; but you are unlikely to have formed an AEM::Application
instance, unless you were thinking of doing some lower-level stuff with Apple events.
app.current
generates an Appscript::Application
instance referring to the host process; this is unlikely to do you any good, because the host process is Ruby and isn’t scriptable.
An Appscript::Application
instance is just about useless for any other purpose than to send it an Apple event. But rb-appscript does provide one Appscript::Application
instance method that does not send that application an Apple event, and for that very reason is extremely useful: is_running?
Exactly what is_running?
does depends on which factory method was used to generate this Appscript::Application
instance:
app.by_name
, app.by_id
, and app.by_creator
: These are all translated by Launch Services to the full pathname of the specified application, so the result is true if the application with that pathname is in fact running. If you send an Apple event to an Appscript::Application
instance generated by one of these factory methods, then if it is not running, it will launch. And even attempting to fetch the application’s dictionary might launch it. Thus, is_running?
is your only chance to prevent a potential target application from being launched.
Here’s a simple example. Let’s say we want to quit iTunes if it is running. Simply sending iTunes the quit
message is not the way to do this, because, although it will indeed cause iTunes to quit, it will first launch iTunes if it isn’t running! That’s time-consuming and wasteful (and ridiculous). The solution is to test is_running?
first:
itu = Appscript.app("iTunes")
itu.quit if itu.is_running?
app.by_pid
: true only if there a process with the process id used to form this instance, and if that process is scriptable.
app.by_url
: true only if an attempt to connect to the specified remote application succeeds. The attempt to connect will put up the authentication dialog if the URL didn’t include a username and password. If you can’t authenticate, or if the process isn’t running, or something else is wrong with this URL, you’ll get back a false result and you’ll know that the Appscript::Application
instance based on this URL isn’t going to be any use to you at the moment.
If the Appscript::Application
instance was generated through app.by_name
, app.by_id
, or app.by_creator
, it was translated by Launch Services to the full pathname of the specified application. To learn this pathname, send the instance the AS_app_data.identifier
method chain. For example:
itu = Appscript.app("iTunes")
puts itu.AS_app_data.identifier #=> /Applications/iTunes.app
If the Appscript::Application
instance was generated through app.by_pid
, it was never resolved to any other form; AS_app_data.identifier
simply returns the process id number. If you really need to know the application’s pathname, you may be able to ask Unix, but be aware that Unix’s notion of the pathname takes it inside the application bundle to the actual executable:
itu = Appscript.app.by_pid(1207)
p `ps -ocomm #{itu.AS_app_data.identifier}`.split[1]
#=> "/Applications/iTunes.app/Contents/MacOS/iTunes"
As we saw in the preceding chapter, rb-appscript works by fetching the target application’s dictionary; that’s why you are then able to talk to the Appscript::Application
object using English-like terms from that dictionary. Unfortunately, the mechanism that rb-appscript uses to do this is somewhat fragile; it can break. If this happens, you might like to use a static dictionary instead.
To allow this, all the Appscript.app
constructors mentioned in this chapter (by_name
, by_id
, and so on, plus the shortcut Appscript.app()
) take an extra optional parameter consisting of a module name. The module in question must have been loaded already, and it must have been created with, or have the format created by, appscript’s Terminology.dump
command.
As a matter of fact, this situation has already arisen with some major scriptable applications. In particular, iTunes 10.6.3 may appear to you to have broken rb-appscript! But fear not. A file containing the module that you need in order to keep using rb-appscript with iTunes is available for download here.
Here’s what to do if iTunes 10.6.3 has broken your use of rb-appscript to script iTunes. Download the file from that link. It’s a zip file, so double-click it to unzip it. The resulting file is called tunes.rb, and it contains a module called Tunes
. (Of course you’re free to rename the file, the module, or both. But let’s assume you don’t do that.) Now, you want to store tunes.rb where it can easily be found by Ruby’s require
command. Of course, any file can easily be found by Ruby’s require
command if you provide a full pathname, but it’s simpler to put it in your Ruby library; that way, you can refer to it simply by its name. On a Mac, the Ruby library is at /Library/Ruby/Site/1.8/ (or whatever your Ruby version number is). So put tunes.rb inside that folder.
Now, before trying to form an iTunes application object, require tunes
; and as you form the iTunes application object, supply the Tunes
module as the extra parameter. Like this:
require `rubygems`
require `appscript`
require `tunes`
itu = Appscript.app.by_name("iTunes", Tunes) # or whatever
That’s all there is to it! You’re back in business.
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!