OS X: Launching Another Application Programmatically

Occasionally an application may require that another application be run. This other application may be some behind-the-scenes “helper” or auxiliary app, or it may be necessary for the user as part of a larger workflow. In this post, we go over some techniques for launching an application programmatically. In the process, I’ll go over a general method for passing parameters to a bundled AppleScript.

An Xcode project for this is available on github.

A Very Simple Approach
The Wrong Way To Do This

For the simplest situations — where you simply want to get another app to launch and become the frontmost app — it’s possible to write just a few lines of code that leverages AppleScript. Here’s a basic function:

void launchWithSystemCall(const char * const app)
{
    if (!app) return;

    char scriptCode[256];
    sprintf(scriptCode, "tell application \\\"%s\\\" to run\n\
                         tell application \\\"%s\\\" to activate",
            app, app);

    char commandLine[256];
    sprintf(commandLine, "osascript -e \"%s\"", scriptCode);

    int result = system(commandLine);
}

This is a two-step process: we have to run the app, and then activate it. If we simply activate, an already-running app will become the frontmost app; however, if the app is not running, it will launch as desired, but not become the frontmost app. We could get around this by simply doing the activate twice, but in the case of a running app, this tends to produce a “flash”.

The osascript shell command simply executes AppleScripts, so we construct our script with the appropriate code, and then use the system call to execute it. So we have a chain of execution here: our app executes the system function, which calls osascript, which then executes the AppleScript code. The plethora of backslashes are a bit jarring to the eye, but are necessary to produce the correct C string to pass through each layer of this onion-like execution path.

Another Simple Approach (Also Wrong)

Because we’re using AppleScript in the approach taken above, it may occur to you that we could simply write a separate AppleScript, and use the system call to execute it:

system("MyScript.scpt appName");

The problem with this is that MyScript.scpt needs to be on the path that system uses. Definitely can be done in some ad-hoc fashion, or the the full path can be provided; but this is no argument in favor of this approach.

The Right Way

Astute readers will object that this (a) is pretty ugly, and (b) does not leverage built-in Objective-C/Cocoa functionality at all. This is quite true, and I only show it in case straight C code is the only alternative, and for pedagogical reasons. Here’s the same approach, done much more “correctly”:

- (void)launchApp:(NSString *)app
{
    if (!app) return;
    NSString *scriptCode  = [NSString stringWithFormat:@"tell application \"%@\" to run\n\
                                                         tell application \"%@\" to activate",
                                                         app, app];

    NSDictionary           *errorDict;
    NSAppleEventDescriptor *returnDescriptor = NULL;
    NSAppleScript          *scriptObject     = [[NSAppleScript alloc] initWithSource:scriptCode];

    returnDescriptor = [scriptObject executeAndReturnError:&errorDict];

    if (returnDescriptor != NULL) {
        ...handle errors, etc...

Here, we’re doing exactly analogous steps to the “wrong way”, but it’s clearly more in the spirit of programming for OS X. Another advantage to this approach is that we can get informative result/status back from the attempt to execute the script code (see this stackoverflow post for an example); in contrast, the system call only returns an error code.

Using a Bundled AppleScript
Hard-Coded Script

Perhaps the “best practice” approach to the problem noted above is to include the script in the app’s bundle, and to use NSAppleScript. I’ll present the simpler version of this first, in which the script itself hard-codes the name of the app to be launched. The idea is simple: we can create an NSAppleScript directly from the bundled script, and execute it as in the second example listed:

- (void)launchApp
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"LaunchHardCodedScript"
                                                     ofType:@"scpt"];

    if (path != nil) {
        NSURL *url = [NSURL fileURLWithPath:path];
        if (url != nil) {
            NSDictionary           *errorDict        = [NSDictionary dictionary];
            NSAppleScript          *scriptObject     = [[NSAppleScript alloc] initWithContentsOfURL:url
                                                                                              error:&errorDict];
            NSAppleEventDescriptor *returnDescriptor = NULL;

            if (scriptObject != nil) {
                returnDescriptor = [scriptObject executeAndReturnError:&errorDict];

                if (returnDescriptor != NULL) {
                    ...handle success or error...

The bundled script is simple:

tell application "TextEdit" to run
tell application "TextEdit" to activate

For such a trivial task as launching an application, creating a bundled script offers little advantage over the construction-on-the-fly nature of the previous approach. However, in the case of more complex script code, keeping the script as a separate file will make for cleaner code, and allow for development and debugging of the script to be done in a more standalone fashion.

 Passing Parameters to An AppleScript

The preceding example is simple, and works fine if there’s only one app you’ll be needing to launch. However, it is possible to create an AppleScript that contains a parameterized function, and to call that function with an argument (in our case, the name of the app to be launched). Such a script might look like this:

on launch_app(app_name)
    tell application app_name to run
    tell application app_name to activate
end launch_app

The code to create the NSAppleScript object and to create the parameter-argument pair comes from some Apple documentation addressing the passing of parameters to scripts in general:

- (void)launchApp:(NSString *)app
{
    if (!app) return;

    // load the script from a resource by fetching its URL from within our bundle
    NSString* path = [[NSBundle mainBundle] pathForResource:@"LaunchParameterizedScript"
                                                     ofType:@"scpt"];

    if (path != nil) {
        NSURL* url = [NSURL fileURLWithPath:path];
        if (url != nil) {
            NSDictionary  *errorDict     = [NSDictionary dictionary];
            NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithContentsOfURL:url
                                                                                 error:&errorDict];

            if (scriptObject != nil) {
                // create the first parameter
                NSAppleEventDescriptor *firstParameter = [NSAppleEventDescriptor descriptorWithString:app];

                // create and populate the list of parameters (in our case just one)
                NSAppleEventDescriptor *parameters = [NSAppleEventDescriptor listDescriptor];
                [parameters insertDescriptor:firstParameter atIndex:1];

                // create the AppleEvent target
                ProcessSerialNumber     psn    = { 0, kCurrentProcess };
                NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber
                                                                                                bytes:&psn
                                                                                               length:sizeof(ProcessSerialNumber)];

                // create an NSAppleEventDescriptor with the script's method name to call,
                // this is used for the script statement: "on launch_app(app_name)"
                // Note that the routine name must be in lower case.
                NSAppleEventDescriptor *handler = [NSAppleEventDescriptor descriptorWithString: [@"launch_app" lowercaseString]];

                // create the event for an AppleScript subroutine,
                // set the method name and the list of parameters
                NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite
                                                                                         eventID:kASSubroutineEvent
                                                                                targetDescriptor:target
                                                                                        returnID:kAutoGenerateReturnID
                                                                                   transactionID:kAnyTransactionID];
                [event setParamDescriptor:handler forKeyword:keyASSubroutineName];
                [event setParamDescriptor:parameters forKeyword:keyDirectObject];

                // call the event in AppleScript
                if (![scriptObject executeAppleEvent:event error:&errorDict]) {
                    // report any errors from 'errors'
                }
            } else {
                // report any errors from 'errors'
            }
        }
    }
}

This is rather a lot of code, consisting primarily of the commands that set up the parameter passing. Apple’s comments describe the role of the various objects/functions involved, so I won’t go into details.

This approach is probably massive overkill for our purpose, but I’ve included it to show how to pass parameters from an app to an AppleScript it executes. More parameters can be passed — note that the code uses a list-type event descriptor:

NSAppleEventDescriptor *parameters = [NSAppleEventDescriptor listDescriptor];

The code block following if (scriptObject != nil) {  can be generalized, then, to create and pass a number of parameters to the script. Such additional parameters may be used directly as part of AppleScript commands, or may be themselves passed as parameters to the application that gets launched by the script.

The Apple technical note I referenced contains details on how to bundle an AppleScript using Xcode, as well.