Choosing between an Execute AppleScript and an Execute JavaScript action

There are several instruments on any good workbench, and we reach for the one that suits a subtask best.

AppleScript has a very mature automation interface, embedded in a small, slow and slightly neglected language with some obvious gaps and stumbling blocks.

JavaScript is a fast and actively updated language, with rich libraries, and a less documented and less polished app Automation interface.

On the one hand we have a good sharp old knife with an awkward handle and a single blade, and on the other a newer penknife, with a wider range of uses, but nothing to match the sharpness of that older blade for one or two things.

( Neither is on the shelf with the power-tools like Haskell and Rust, Lisp and C – this is just scripting, after all – small-scale coffee-break work – all you need is the old Opinel or new Swiss Army knife that you use for picnics … )

The main real gap in Apple’s Automation object for JS is that it can’t set a value across a whole collection in a single Apple Event, and whenever the speed and convenience of doing this are key, it’s probably an Execute AppleScript action that you want to reach for.

A classic example: toggling the expansion of many rows in OmniOutliner:

tell application "OmniOutliner"
    if (count of documents) > 0 then
        tell front document
            set toggle to not expanded of first row
            
            -- SET A VALUE RIGHT ACROSS A COLLECTION
            -- (using just one Apple Event)
            
            set expanded of rows to toggle  -- (Applescript shines here)
            
            if toggle then
                "Expanded"
            else
                "Collapsed"
            end if
        end tell
    else
        "No outlines open in OmniOutliner"
    end if
end tell

We can certainly do this in JS,

(() => {

    // TOGGLING OO ROWS ------------------------------------------------------
    const
        rows = Application('OmniOutliner')
        .documents[0]
        .rows,
        toggled = !rows[0].expanded();

    return (
        rows()
        .map(x => x.expanded = toggled),
        toggled ? 'Expanded' : 'Collapsed'
    );
})();

but with larger outlines it’s noticeably slower (one Apple Event per row), which is distracting.

I personally also still often use a large old AppleScript which relies quite heavily on setting properties quickly across large collections of lines and shapes in OmniGraffle.

(omniJS is now even faster, but that’s another story, and my old Applescript still works, at least on the macOS version of OmniGraffle).

When setting properties over large collections is not an issue, I always reach for JavaScript – which I find more flexible, faster, with fewer gotchas, richer libraries and somehow just a more pleasant experience.

But, occasionally, even within JavaScript, I still reach for AppleScript. There are (real, though rather rare) cases when some part of a particular application’s automation interface just doesn’t work properly with JavaScript.

(pre-omniJS for example, there was an issue in OmniGraffle with automating the creation of parent-child links between shapes. It worked in AppleScript, and somehow choked on a datatype conversion in JavaScript).

One way of doing this is to run an AppleScript snippet from inside an Execute Javascript for Automation with something like an evalASMay function. Here’s an example of calling it, although this is a case in which I would probably just use AppleScript :slight_smile:

Using an Applescript snippet from inside an Execute JXA action:

(() => {
    'use strict';

    // GENERIC FUNCTIONS -----------------------------------------------------

    // evalASMay :: String -> Maybe String
    const evalASMay = s => {
        const
            error = $(),
            result = $.NSAppleScript.alloc.initWithSource(s)
            .executeAndReturnError(error),
            e = ObjC.deepUnwrap(error);
        return e ? {
            nothing: true,
            msg: e.NSAppleScriptErrorBriefMessage
        } : {
            nothing: false,
            just: ObjC.unwrap(result.stringValue)
        };
    };

    // intercalate :: String -> [String] -> String
    const intercalate = (s, xs) => xs.join(s);

    // TOGGLE EXPANSION ACROSS OMNIOUTLINER ROW COLLECTION -------------------
    const
        ds = Application('OmniOutliner')
        .documents,
        mbBool = ds.length > 0 ? {
            nothing: false,
            just: ds[0].rows[0].expanded()
        } : {
            nothing: true
        };

    return mbBool.nothing ? (
        'No document open in OmniOutliner'
    ) : (() => {
        const strName = evalASMay(
                'tell application "OmniOutliner" to tell front document\n\
                    set expanded of rows to ' + !mbBool.just + '\n\
                    name\n\
                 end tell'
            )
            .just;
        // Value -------------------------------------------------------------
        return intercalate(
            ' -> ', [strName, (mbBool.just ? 'collapsed' : 'expanded')]
        )
    })();
})();

and, of course, if you want to reach for bits of JavaScript libraries from inside an Execute AppleScript action, you can do the same, in the opposite direction.

Using a Javascript snippet from inside an Execute Applescript action:

(evalJSMay() will only work if you include the line use framework "AppKit" at the top of your AS code)

use framework "AppKit"

--  Javascript code -> Maybe AppleScript string (or Nothing with error msg)

-- evalJSMay :: String -> Maybe String
on evalJSMay(strCode)
    set ca to current application
    set oScript to ca's OSAScript's alloc's initWithSource:strCode ¬
        |language|:(ca's OSALanguage's languageForName:("JavaScript"))
    
    set {blnCompiled, compileError} to oScript's compileAndReturnError:(reference)
    if blnCompiled then
        set {descriptor, evalError} to oScript's executeAndReturnError:(reference)
        if (evalError is missing value) then
            {nothing:false, just:stringValue of descriptor as text}
        else
            {nothing:true, msg:NSLocalizedDescription of evalError as text}
        end if
    else
        {nothing:true, msg:NSLocalizedDescription of compileError as text}
    end if
end evalJSMay


-- TEST ------------------------------------------------------------------
on run
    evalJSMay("Math.cos(.5)")
    --> {nothing:false, just:"0.8775825618903728"}
    
    evalJSMay("encodeURIComponent('A string of some kind')")
    --> {nothing:false, just:"A%20string%20of%20some%20kind"}
end run
4 Likes

I'm not sure that this is an accurate characterization of AppleScript, particularly if you make use of ASObjC (AppleScriptObjectiveC). And, there are some scripting gurus who believe JXA has some basic flaws, or limitations.

So, I guess I'm saying that I don't view either AppleScript or JXA to the extremes that @ComplexPoint does. IMO, AppleScript is much, much better that you might think, and JXA is not as perfect as you might think. I use both.

##asobjc

  • ASObjC is extremely fast. I have seen it process thousands of items/records in much less than 1 sec, often < 0.1 sec.
  • It gives you access to almost all of the macOS system-level objects, and is very proficient at file/folder management, plist/dictionary management, and much, much more.
  • While you can use ObjC in both AppleScript and JXA, much, much more has been written for AppleScript than JXA.
  • Plus, and this is a huge plus, if you are trying to use/write ASObjC it is fairly easy to get help from a world-class guru: @ShaneStanley. It is much harder to get help with JXA ObjC.

###Greatly Increasing Speed of AppleScript
One thing that can make a tremendous difference in the speed of AppleScript, is how often, how many times, you send a command to an application object. By this I mean putting commands/statements inside of a tell block.

I have seen the speed increased (execution time reduced) by more than 10X just by judicious use of commands to app objects. Often you can get the data you need from the app with one or two statements, and then do the remaining processing outside of the tell block.

##applescript Resources
I'm sure Chris Stone (@ccstone) will be along soon to offer some more insights into using AppleScript. Chris is also a world-class guru in AppleScript, who, as many of you know, hangs out here in the KM forum, and often offers valuable help in AppleScript, KM, RegEx, and other topics.

On the contrary, there is very little help for JXA. The JXA "community" (such as it is), is very, very small compared to the AppleScript community. That means it is harder to find qualified help for JXA. @ComplexPoint has provided more help here in the KM forum than probably available anywhere else.

Choosing Which to Use and Learn

Having said all that, I like both AppleScript and JavaScript for Automation (JXA). I don't find that speed/performance is much of a discriminator, but both tools do have their advantages.

I do agree with @ComplexPoint on one point: If you don't know either AppleScript or JavaScript, then, IMO, JavaScript and JXA is easier to learn. Of course, if you already know JavaScript, but not JXA, then learning how to use JXA to access app objects is all that you need to add to your skill set.

Development and Debugging Tools

One counterpoint in favor of AppleScript: You have available (at extra cost) a great developlment and debugging tool: Script Debugger 6. If you expect to be doing AppleScript development often (let's say > 10 times/month), then SD6 will greatly speed up your development, and greatly reduce your frustrations. If you are interested, a free 30-day full-featured trial is available.

There is nothing comparable for JXA. Yes, there are some great JavaScript IDEs, but none of them help you with the access to app models. Safari does offer a JXA debugger that provides limited help.

Bottom Line

The great thing is that KM gives you easy access to run both AppleScript and JXA scripts (and other script languages). So, you can choose either, or both, whatever works best for you. And recently, with KM 8.0.3+, @peternlewis has given us a huge update to the KM Editor scripting model, which is available to both AppleScript and JXA.

3 Likes