macOS JavaScript for Automation (JXA) Notes
Overview
There is a macOS feature called the Open Scripting Architecture (OSA) which provides an infrastructure for intercommunication and automation of macOS software. One of the officially supported OSA languages is JavaScript and its use in this context is called “JavaScript for Automation” which is often abbreviated to “JXA”.
The idea is that instead of using AppleScript, you can program & automate macOS using a more modern language like JavaScript. This gives the programmer access to the enormous ecosystem of javascript libraries and tools in addition to all the modern optimizations and language features of ES6-ish JavaScript. JXA also provides access to a built-in Objective-C bridge that enables access to the file system and the ability to call into the Cocoa frameworks and plain C functions. JXA can basically do anything native code can. It’s a bit like node without needing to install node.
I’m really just learning about JXA now, a full 5 and a half years after it was initially released as part of macOS 10.10 “Yosemite”. My initial excitement has quickly been tempered by some complications:
- The underpinnings are based on Apple events, so you’re still often dealing with tons of opaque proxy objects.
- This means you have proxies to objects and collections that aren’t directly accessible in javascript (but apple has added some features like the
.whose
method (described below) to access some of these). It can sometimes be tricky to understand what is a javascript-level collection and what is an AppleEvent-level collection. - The future of JXA (and OSA) is in question:
- 2014-10-16: JXA initial release as part of macOS 10.10 “Yosemite”
- 2015-09-30: JXA gets some very minor updates in macOS 10.11 “El Capitan”
- 2016-11-16: Sal Soghoian is fired and Apple eliminates the “Product Manager of Automation Technologies” position for “business reasons”. This seems to signal a major contraction of Apple’s support for macOS automation tools.
- Since then: Crickets mostly. No updates, JXA is mostly missing from Apple user and developer docs, industry adoption is limited. Apple has been moving further and further away from built-in scripting support and they’ve been moving toward requiring notarization for everything.
- Like many other automation technologies JXA is a security concern. As is the case with PowerShell, attackers and malware now mostly use powerful interpreters like JXA to explore target machines and their connected networks with little or no footprint.
All that said, JXA support is still there in macOS as of this writing… in all it’s beautiful/powerful/terrible majesty.
Resources
- ⭐ The JavaScript for Automation Cookbook on github
- ⭐ Apple’s best docs on it are here: OS X 10.10 Release Notes
- Apple also has these documents: OS X 10.11 Release Notes, Mac Automation Scripting Guide
- If you are using Node, check out the node jxa package.
Notes
Many of these come from the resources above, so really do check out the JXA Cookbook and the resources it links to.
Access the REPL
Access the REPL from the terminal with:
osascript -il JavaScript
Shebang Support
JXA scripts can run using the shebang mechanism. Begin your scripts with this line to be able to invoke them directly:
#!/usr/bin/env osascript -l JavaScript
For example:
$ cat neato.js
#!/usr/bin/env osascript -l JavaScript
console.log("this is cool")
$ chmod +x neato.js
$ ./neato.js
this is cool
If you put your code in a function called run, it will be invoked automatically when you run the script and it will be passed the argument vector.
#!/usr/bin/env osascript -l JavaScript
function run(argv) {
console.log("this is cool", argv.join("-"))
}
running it with some arguments produces:
$ ./neato.js is it not?
this is cool is-it-not?
Object Specifiers
Mostly the things that get thrown around in JXA are called Object Specifiers. They represent data that the Apple Events layer interacts with and they look like this:
Application("Mail").accounts.byId("A8ECDFBC-BF8F-437D-BB52-525E0D1EEDA0").mailboxes.byName("testbox")
From the Apple docs:
Many objects in the JavaScript for Automation host environment refer to external entities, such as other applications, or windows or data inside of those other applications. When you access a JavaScript property of an application object, or of an element of an application object, a new object specifier is returned that refers to the specific property of that object.
…it continues:
You access properties of scripting objects as JavaScript properties using dot notation. As described above, the returned object is an object specifier — a reference to the accessed property—rather than the actual value. To send the get event to the external entity and return its value, call the property as a function:
subject = Mail.inbox.messages[0].subject()
Similarly, setting a property sends the set event to the external entity with the data you want to set.
Mail.outgoingMessages[0].subject = 'Hello world'
Here is an example showing how to get a property from every element of an array (in this case, the subject of every message in Mail’s inbox).
subjects = Mail.inbox.messages.subject()
You can access an Object Specifier’s text representation with Automation.getDisplayString()
.
Here’s a quick example:
>> var firstMailbox = Application("Mail").accounts[0].mailboxes[0]()
>> console.log(firstMailbox)
!! Error on line 1: Error: Can't convert types.
>> console.log(Automation.getDisplayString(firstMailbox))
Application("Mail").accounts.byId("978F1418-FE48-4A59-A08F-4EA23D387DDC").mailboxes.byName("testbox")
Poking around
- Try accessing the
.properties
of an object. - Try just expressing the object in the REPL to see what comes back.
- Observe the apple events that get fired as your script executes using
Script Editor.app
Global Scope Notes
This is mostly stuff from the REPL.
Automation
Automation = {
"$": "function $",
"Application": "function anonymous",
"Library": "function anonymous",
"ObjC": {"registerSubclass": "function anonymous",
"ObjectSpecifier": "object ObjectSpecifierConstructor",
"Path": "function anonymous",
"Progress": "object Progress",
"Ref": "function anonymous",
"bindFunction": "function anonymous",
"block": "function anonymous",
"castObjectToRef": "function anonymous",
"castRefToObject": "function anonymous",
"deepUnwrap": "function anonymous"},
"delay": "function anonymous",
"dict": "function anonymous",
"getDisplayString": "function anonymous",
"import": "function anonymous",
"initializeGlobalObject": "function anonymous",
"interactWithUser": false,
"log": "function anonymous",
"super": "function anonymous",
"unwrap": "function anonymous",
"wrap": "function $",
}
Application
Class methods:
Application.currentApplication
Object methods (callable on instantiated Application objects):
Application = {
"activate": "function anonymous",
"commandsOfClass": "function anonymous",
"displayNameForCommand": "function anonymous",
"displayNameForElementOfClass": "function anonymous",
"displayNameForPropertyOfClass": "function anonymous",
"elementsOfClass": "function anonymous",
"frontmost": "function anonymous",
"id": "function anonymous",
"includeStandardAdditions": false,
"launch": "function anonymous",
"name": "function anonymous",
"parentOfClass": "function anonymous",
"propertiesOfClass": "function anonymous",
"quit": "function anonymous",
"running": "function anonymous",
"strictCommandScope": false,
"strictParameterType": false,
"strictPropertyScope": false,
"version": "function anonymous",
}
Library
Prototype:
{
"displayNameForPropertyInClass": "function anonymous",
"enumeratorsForEnumeration": "function anonymous",
"parameterNamesForCommand": "function anonymous",
"parameterTypeForNameInCommand": "function anonymous",
"propertyTypeForNameInClass": "function anonymous",
}
Path
Just the constructor and the toString()
method.
Progress
Just an object
ObjectSpecifier
Just an object
delay
Just a function
console.log
Just a function
ObjC
All class methods:
ObjC = {
"$": "function $",
"bindFunction": "function anonymous",
"block": "function anonymous",
"castObjectToRef": "function anonymous",
"castRefToObject": "function anonymous",
"deepUnwrap": "function anonymous",
"dict": "function anonymous",
"import": "function anonymous",
"interactWithUser": false,
"Ref": "function anonymous",
"registerSubclass": "function anonymous",
"super": "function anonymous",
"unwrap": "function anonymous",
"wrap": "function $",
}
Ref
A class method: Ref.equals()
And object methods:
Ref = {
"getProperty": "function anonymous",
"setProperty": "function anonymous",
"type": undefined,
}
$
Just a function
Array Filtering with .whose({...})
From the apple docs:
Example that “find[s] all of the messages in your inbox that contain the word JavaScript or the word Automation in the subject”:
Mail.inbox.messages.whose({
_or: [
{subject: {_contains: "JavaScript"}},
{subject: {_contains: "Automation"}}
]
})
Supported Filter Operators
operator | example |
---|---|
_equals '=' literal matching |
{ name: { _equals: 'JavaScript for Automation' } } { name: { '=': 'JavaScript for Automation' } } { name: 'JavaScript for Automation' } |
_contains |
{ name: { _contains: 'script for Auto' } } |
_beginsWith |
{ name: { _beginsWith: 'JavaScript' } } |
_endsWith |
{ name: { _endsWith: 'Automation' } } |
_greaterThan '>' |
{ size: { _greaterThan: 20 } } { size: { '>': 20 } } |
_greaterThanEquals '>=' |
{ size: { _greaterThanEquals: 20 } } { size: { '>=': 20 } } |
_lessThan '<' |
{ size: { _lessThan: 20 } } { size: { '<': 20 } } |
_lessThanEquals '<=' |
{ size: { _lessThanEquals: 20 } } { size: { '<=': 20 } } |
_and _or (both require 2 args) |
{ _and: [ {name: 'Apple'}, {size: {'<': 20}} ] } { _or: [ {name: 'Apple'}, {name: {_contains: 'JavaScript'}} ] } |
_not |
{ _not: [ {name: 'Apple'}, {name: {_contains: 'JavaScript'}} ] } |
The _and
, _or
, and _not
keys require arrays as their value, additionally the _and
and _or
arrays requires at least two elements in that array.