Mac Automation with Pascal


Contents

Introduction
AppleScript
JavaScript
Scripting under Pascal control
Pascal and Scripting Bridge
Misc. notes


Introduction

Many apps on the Mac can be automated, meaning you can write scripts to control them externally. Apps that can be manipulated this way include all of Apple's free apps, such as the iWork suite (Numbers, Pages, Keynote), Safari, iTunes, GarageBand and others. Even Xcode can be scripted. Notably, Microsoft Office apps can be completely controlled by external software, similar to what you can do with them via COM Automation on Windows. This is true for Excel, Word, PowerPoint, OneNote and Outlook.

You can use AppleScript, JavaScript or Objective C to control apps on the Mac. Since Free Pascal has an Objective Pascal dialect, you can use it in place of Objective C. You can also execute AppleScript and JavaScript scripts under Pascal control.

The sections below include code examples in several languages for controlling the Numbers spreadsheet app. All Macs should already have Numbers. If not, just install it via the Mac App Store.

Source code for the examples is available here.


AppleScript

Traditionally scripting on the Mac was done with AppleScript, its built-in scripting language. This is still the most reliable and best-documented way of scripting on the Mac. You can create an .applescript file with Script Editor (under Applications/Utilities), or you can use a text editor such as Visual Studio Code, which also supports AppleScript syntax highlighting. You can run the script in Script Editor or from the command line with the osascript utility.

Any app that can be scripted includes a scripting "dictionary". You can open any dictionary in Script Editor to see the AppleScript, JavaScript or Objective C syntax for the classes and properties available to your scripts with that app. And since the dictionary is just an XML file, you can also open it with a text editor. For example, Numbers' dictionary is file Numbers.sdef in the Numbers.app/Contents/Resources folder.

Here's an example script that manipulates the Numbers app. In AppleScript, code comments are indicated by double hyphens (--).

try
  tell application "Numbers"

    log name & " " & version --output app's name and version

    set numDoc to make new document with properties {name:"My Doc"} --create new document

    set name of sheet 0 of numDoc to "My Sheet" --change sheet's tab

    set numCell to cell 2 of row 2 of table 0 of sheet 0 of numDoc
    --get upper left-most non-header cell of doc's first sheet

    tell numCell --set cell's properties
      set value to 1.23
      set font size to 15
    end tell

    activate --move app to foreground

  end tell
	
on error errMsg number errorNumber
  return errMsg
end try
To run this example script yourself, you can open file testnumbers.applescript in Script Editor, then click the Run button. You can also run it from the command line in a Terminal window like this:

osascript testnumbers.applescript


JavaScript

OS X 10.10 (Yosemite) introduced support for JavaScript in Script Editor. OS X 10.11 (El Capitan) added support for debugging JavaScript in Safari. This means that you can now develop scripts using a more familiar language.

Here's the Numbers example as JavaScript:

try {
  var numApp = Application("Numbers");

  console.log(numApp.name() + " " + numApp.version());

  var numDoc = numApp.Document({name:"My Doc"}).make();

  numDoc.sheets[0].name = "My Sheet";

  var numCell = numDoc.sheets[0].tables[0].rows[1].cells[1];

  numCell.value = 1.23;
  numCell.fontSize = 15;

  numApp.activate();

} catch(e) {
  e.message;
}
Like the AppleScript example above, you can open file testnumbers.scpt in Script Editor and click the Run button. Or you can run it from the command line:

osascript testnumbers.scpt


Scripting under Pascal control

The Foundation framework includes the Objective C NSAppleScript class. You can use this class in your Pascal app to execute a file containing AppleScript or a text string in your program containing AppleScript. And despite its name, you can also use NSAppleScript to execute JavaScript script files (.scpt).

Here's an example Pascal program that can run any of the example script files.

program TestScriptFile;

{$mode objfpc}{$H+}
{$modeswitch objectivec2}

uses
  SysUtils,
{$IFDEF NoCocoaAll}
  Foundation;
{$ELSE}
  CocoaAll;
{$ENDIF}

function RunScript(const ScriptFName : string;
                     out ErrorMsg    : string) : Boolean;
var
  ScriptURL : NSURL;
  ScriptObj : NSAppleScript;
  ErrorInfo : NSDictionary;
  EventDesc : NSAppleEventDescriptor;

begin
  Result := False;
  ErrorMsg := '';

  if (Copy(ScriptFName, 1, 5) = 'http:') or
     (Copy(ScriptFName, 1, 6) = 'https:') then
    ScriptURL := NSURL.URLWithString(NSSTR(PAnsiChar(ScriptFName)))
  else
    ScriptURL := NSURL.fileURLWithPath(NSSTR(PAnsiChar(ScriptFName)));

  ScriptObj := NSAppleScript.alloc.initWithContentsOfURL_error(
                ScriptURL, @ErrorInfo);
  if not Assigned(ScriptObj) then
    begin
    ErrorMsg := 'Unable to load script:'#10 +
                NSString(ErrorInfo.objectForKey(NSAppleScriptErrorMessage)).UTF8String;
    Exit;
    end;

  try
    EventDesc := ScriptObj.ExecuteAndReturnError(@ErrorInfo);
    if not Assigned(EventDesc) then
      begin
      ErrorMsg := 'Unable to execute script:'#10 +
                  NSString(ErrorInfo.objectForKey(NSAppleScriptErrorMessage)).UTF8String;
      Exit;
      end;
    if (EventDesc.stringValue.UTF8String <> '') and
       (EventDesc.stringValue.UTF8String <> 'true') then
      begin
      ErrorMsg := 'Error executing script:'#10 +
                  EventDesc.stringValue.UTF8String;
      Exit;
      end;

    Result := True;

  finally
    ScriptObj.release;
  end;
end;


var
  ErrorMsg : string;
begin
  if ParamStr(1) = '' then
    begin
    WriteLn('No script file specified.');
    Halt(1);
    end;

  if not RunScript(ParamStr(1), ErrorMsg) then
    begin
    WriteLn(ErrorMsg);
    Halt(1);
    end;
end.

After compiling testscriptfile.pas, you can use this program to run a script from the command line like this:

./testscriptfile testnumbers.applescript

Embedded AppleScript

If you want to generate your script on the fly and dispense with an external script file, you can put AppleScript in a text string. Use the NSAppleScript method initWithSource to load the script instead of initWithContentsOfURL_error.

Here's a fragment of code from the testscriptembed.pas example that shows how it's done to control the Pages app:

  ScriptStr :=
   'try'#10 +
   '  tell application "Pages"'#10 +
   '    log name & " " & version'#10 +
   '    set pagesDoc to make new document'#10 +
   '    tell pagesDoc'#10 +
   '      set size of body text to 15'#10 +
   '      set body text to "Howdy there!"'#10 +
   '    end tell'#10 +
   '    activate'#10 +
   '  end tell'#10 +
   'on error errMsg number errorNumber'#10 +
   '  return errMsg'#10 +
   'end try';

  ScriptObj := NSAppleScript.alloc.initWithSource(NSSTR(PAnsiChar(ScriptStr)));


Pascal and Scripting Bridge

Scripting Bridge is a framework that allows you to control other programs from compiled code rather than using AppleScript or JavaScript, but it requires some preparation before you can start programming.

The first step is to create a header file for the app you want to script. For example, to create Numbers.h, enter this at the command line:

sdef "/Applications/Numbers.app" | sdp -fh --basename Numbers

The resulting Numbers.h header file can then be used with Objective C and Swift code.

Since Pascal can't work directly with a header file, you also have to create an interface unit by parsing the header file. You can use the framework parser from here to do that.

Finally, since the framework parser typically doesn't do a perfect job, you'll probably need to manually tweak the resulting Pascal interface unit to get it to compile. This has already been done for you with Numbers, Pages and Keynote; Pascal interface units for those apps are included with the examples.

Here's the Numbers example as Pascal (testnumbers.pas):

program TestNumbers;

{$mode objfpc}{$H+}
{$modeswitch objectivec2}

uses
  SysUtils,
{$IFDEF NoCocoaAll}
  Foundation,
{$ELSE}
  CocoaAll,
{$ENDIF}
  ScriptingBridge,
  Numbers;

var
  NumApp  : NumbersApplication;
  NumDoc  : NumbersDocument;
  NumCell : NumbersCell;
begin
  try
    NumApp := SBApplication.applicationWithBundleIdentifier(NSSTR('com.apple.iWork.Numbers'));
    if not Assigned(NumApp) then
      begin
      WriteLn('Unable to start Numbers.');
      Halt(1);
      end;

     {Output app's name and version}
    WriteLn(NumApp.name.UTF8String, ' ', NumApp.version.UTF8String);

     {Create and add a document}
    NumDoc := class_createInstance(
               NumApp.classForScriptingClass(NSSTR('document')), 0).
                initWithProperties(NSDictionary.dictionaryWithObject_forKey(
                                    NSSTR('My Doc'), NSSTR('name'))).autorelease;
    NumApp.documents.addObject(NumDoc);

     {Change sheet's tab}
    NumbersSheet(NumDoc.sheets.objectAtIndex(0)).setName(NSSTR('My Sheet'));

     {New document will contain one sheet with one table. Get second cell
       of second row in table.}
    NumCell := NumDoc.sheets.objectAtIndex(0).tables.objectAtIndex(0).
                rows.objectAtIndex(1).cells.objectAtIndex(1);

     {Set cell's properties}
    NumCell.setValue(NSNumber.numberWithDouble(1.23));
    NumCell.setFontSize(15);

     {Move app to foreground}
    NumApp.activate;

  except on E:Exception do
    begin
    WriteLn(E.Message);
    Halt(1);
    end;
  end;

end.


Misc. notes

  1. While many Mac apps are scriptable, they may not be fully scriptable. For example, although you can create nice charts with Numbers interactively, the NumbersChart class is empty. By contrast, the Microsoft Office apps for Mac are fully scriptable, on a par with their Windows counterparts. (The Excel.sdef dictionary file is 20 times the size of the Numbers.sdef file.)

  2. Since programming with the Scripting Bridge can be trickier than working with AppleScript in Script Editor, you might start by making sure what you want to do can be done in AppleScript before you try to do it in Pascal.

  3. Note that there are restrictions on scripting in a sandboxed app. Follow these suggestions if your app will be sandboxed.

  4. To simplify scripting with Swift, use the tools described in this article.

Copyright 2018 by Phil Hess.

macpgmr (at) icloud (dot) com

First posted Feb. 20, 2018; last edited Feb. 20, 2018.

Code syntax highlighting done with highlight.js.