JXA’s parenthesis paradox
May 6, 2022 at 1:16 PM by Dr. Drang
I don’t like programming in AppleScript in general, but there is one construct that I’ve always liked. Because I’m not a computer scientist, I don’t know what to call it,1 but I can give an example that came up recently:
applescript:
tell application "System Events" to get the name of every process whose visible is true
The System Events
preamble doesn’t matter. It’s the
_____ of _____ whose _____ is _____
that I find interesting. It’s compact, easy to understand, and, I would say, the most useful example of AppleScript’s “English-like” syntax.
In trying to write the JavaScript for Automation (JXA) equivalent of this, I learned (sort of) how parenthesis work in JXA when dealing with properties and filters by reading this excellent article by Christian Kirsch. I’m writing about it here before I forget.
Let’s start with something simpler:
applescript:
tell application "System Events" to get every process
This returns a long list—over 100 items—which I’m going to condense and format as one item per line:
{application process "loginwindow" of application "System Events"
application process "ViewBridgeAuxiliary" of application "System Events"
application process "NotificationCenter" of application "System Events"
.
.
.
application process "Dictionary" of application "System Events"
application process "com.apple.WebKit.WebContent" of application "System Events"
application process "System Events" of application "System Events"}
The JXA equivalent is also pretty simple:
javascript:
Application('System Events').processes();
It’s result is recognizable as the JavaScript equivalent to the AppleScript result:
[Application("System Events").applicationProcesses.byName("loginwindow")
Application("System Events").applicationProcesses.byName("ViewBridgeAuxiliary")
Application("System Events").applicationProcesses.byName("NotificationCenter")
.
.
.
Application("System Events").applicationProcesses.byName("Dictionary")
Application("System Events").applicationProcesses.byName("com.apple.WebKit.WebContent")
Application("System Events").applicationProcesses.byName("System Events")]
So far, so good. Now let’s add one piece of complexity. Instead of returning a complete description of each process, let’s get just their names. In AppleScript, that’s easy:
applescript:
tell application "System Events" to get the name of every process
The the
isn’t necessary—get name of every process
would work just as well—but it adds to the readability. This returns a list of the same length but with simpler items.
{"loginwindow"
"ViewBridgeAuxiliary"
"NotificationCenter"
.
.
.
"Dictionary"
"com.apple.WebKit.WebContent"
"System Events"}
In JXA, we do this
javascript:
Application('System Events').processes.name();
to get a JavaScript list of the same items:
["loginwindow"
"ViewBridgeAuxiliary"
"NotificationCenter"
.
.
.
"Dictionary"
"com.apple.WebKit.WebContent"
"System Events"]
To me, the tricky part of this is how we removed the parentheses from processes
before invoking names
. Trying
javascript:
Application('System Events').processes().name();
results in an error:
TypeError: Application('System Events').processes().name is not a function.
This is kind of misleading, as it suggests the problem is with name
when the problem is really with processes()
. We know from our previous example that
javascript:
Application('System Events').processes();
returns a JavaScript array. It’s an array of processes, yes, but it’s still an array. And name()
can only be applied to processes, not to an array of processes. So to get around this while still providing reasonable compact syntax, JXA provides processes
without the parentheses as an object specifier to which name()
can be applied on an item-by-item basis. Again, see Christian Kirsch’s article for a better explanation.
Now we go for the last step. In AppleScript, it’s
applescript:
tell application "System Events" to get the name of every process ¬
whose visible is true
which returns (not condensed)
{"Messages"
"Calendar"
"Finder"
"Reminders",
"iTerm2"
"BBEdit"
"Mail"
"Contacts",
"Preview"
"Safari"
"Script Editor"
"Dictionary"}
(While it’s not the point of the post, this is the list of apps I happen to be running right now—the ones in the Dock. Invoking this line of AppleScript at some other time would return a different list.)
In JXA, the command is
javascript:
Application('System Events').processes.whose(
{visible: true}).name();
and the result is
["Messages"
"Calendar"
"Finder"
"Reminders"
"iTerm2"
"BBEdit"
"Mail"
"Contacts"
"Preview"
"Safari"
"Script Editor"
"Dictionary"]
Because the name of the process is what we want to return, name
has to go last in the chain of calls, and it’s the one that gets the parentheses. We’re applying the whose
filter to the processes, so it has to go after processes
. The properties we’re filtering on (just visible
in this case) are set up as a JavaScript object that’s passed as an argument to whose
.
Suppose we had two criteria for our filter? Let’s say we want all the visible processes that start with the letter M (which will return just Mail and Messages). In AppleScript, it’s easy to add an extra clause:
applescript:
tell application "System Events" to get the name of every process ¬
whose visible is true and name begins with "M"
In JXA, the syntax gets nasty:
javascript:
Application('System Events').processes.whose(
{visible: true, name: {_beginsWith: "M"}}).name();
I think it’s obvious that we need to add a second piece to the object passed to whose
, but that it should be
javascript:
name: {_beginsWith: "M"}
is anything but obvious to me. The rule for constructing this sort of filter—where a function is applied to the property—is that its key is the property we’re going to filter on, and the value is yet another object whose key is the function to be applied to the property and whose value is the argument to that function.
But wait, there’s more! The name of the function comes from the AppleScript terminology, where multi-word functions are smashed together using camelCase, and the whole thing is preceded by an underscore. Thus,
applescript:
name begins with "M"
becomes
javascript:
name: {_beginsWith: "M"}
Simple, right?
The more I delve into JXA, the more I think I should stick with AppleScript and get more clever about using do shell script
with Perl or sed for string processing.2
Update 6/11/2022 9:58 PM
There’s some followup in my next post.
-
Something to do with properties and filters, I imagine. ↩
-
By the way, I know there are AppleScript libraries for string processing. Ages ago, I used Satimage’s Smile libraries, but as Apple began breaking longstanding pieces of automation, I got leery of using third-party add-ons. As far as I know, though, Smile’s text and regex functions are still working just fine. ↩