Is it possible to call javascript from AppleScript?

Hi, I’m new on this forum, so maybe a short introduction. I’m not new to programming, but I’m new to AppleScript. I’ve used OS X for 10+ years, and I also made an iPhone app, but never got around to use AppleScript. I must say, I’m amazed a lot of times. Now to my question:

Is it possible to call from my applescript code to call in someway some javascript code? I’ve tried creating a scripting library in javascript (created in Script Editor, and choose language JavaScript), but I only managed to call it from another JavaScript-script. Calling from AppleScript fails…

I also tried run script some_alias_to_my_js_file in "JavaScript" and this works, but as soon as I want to pass parameters like run script some_alias_to_my_js_file with parameters {"isPcNrToev"} in "JavaScript" it fails with error -4960. The javascript contains the code function(args) { return 42; }.

I’d really like to call some JavaScript from my AppleScript, since I know it well and it can do some handy things (think regular expressions). I also prefer a Scripting Library, because then I can have multiple functions in one file…

Any ideas?

This probably not what you want, but just in case . . .

I have found that you can call handlers in an AppleScript Script Library from a JXA script. Load the AppleScript Script Library just like you would a JXA library.

@alldritt, it would be totally awesome if Script Debugger would also support JXA scripts. I don’t know about anyone else, but I would be more than willing to pay 2X for a SD that supports both.

I don’t think there’s a way using run script – that’s only going to call the run() function anyway. But it can be done using AppleScriptObjC.

So here’s a simple script saved as a script library in JSTest.scpt:

function doIt(arg1, arg2) { 
	return arg1 + arg2; 
}

You need to get the path to the library, which can be one of several possibilities, and AppleScript throws an error if you try the normal script methods. But you can take advantage of that:

try
	script "JSTest.scpt" -- errors if lib is JXA
on error from jsLib
	set jsLibPath to POSIX path of (jsLib as alias)
end try

Then you need to use OSAScript to load the script and execute the function. So a full script might look like this:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "OSAKit"
use scripting additions
property myJSLib : missing value

try
	script "JSTest.scpt"
on error from jsLib
	set jsLibPath to POSIX path of (jsLib as alias)
	set {my myJSLib, theError} to current application's OSAScript's alloc()'s initWithContentsOfURL:(current application's |NSURL|'s fileURLWithPath:jsLibPath) |error|:(reference)
	if myJSLib is missing value then error (theError's localizedDescription()) as text
end try

my callFunction:"doIt" withArgs:{2, 2} inJSLib:myJSLib
--> 4

on callFunction:funcName withArgs:argsList inJSLib:myJSLib
	set {theResult, theError} to myJSLib's executeHandlerWithName:funcName arguments:argsList |error|:(reference)
	if theResult is missing value then error (theError's localizedDescription()) as text
	-- hack to convert from descriptor to correct AS class
	return item 1 of ((current application's NSArray's arrayWithObject:theResult) as list)
end callFunction:withArgs:inJSLib:

Think NSRegularExpression. Assuming you used Objective-C for your iPhone app, you should be familiar with the Cocoa frameworks, and you have pretty good access to them in AppleScript.

Are these as easy to use as JavaScript Regular Expressions?

Ease is in the eye of the beholder, but they’re not complex. A replace operation is simple:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

set aString to current application's NSString's stringWithString:"It costs $4.50 each way"
set aString to (aString's stringByReplacingOccurrencesOfString:"\\$([0-9,.]+)" withString:"€$1" options:(current application's NSRegularExpressionSearch) range:{0, aString's |length|()}) as text

Most of the work is going to and from NSStrings.

Finds are a bit more complex in that the results are returned as ranges, which you may then need to use to extract substrings:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

set aString to current application's NSString's stringWithString:"It costs $4.50 each way"
set theRange to aString's rangeOfString:"(?<=\\$)[0-9,.]+" options:(current application's NSRegularExpressionSearch)
set theResult to (aString's substringWithRange:theRange) as text

Or if there could be multiple matches:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

set aString to current application's NSString's stringWithString:"It costs $4.50 each way"
set {theExpr, theError} to current application's NSRegularExpression's regularExpressionWithPattern:"\\$([0-9,.]+)" options:0 |error|:(reference)
set theMatches to theExpr's matchesInString:aString options:0 range:{0, aString's |length|()}
set theResults to {}
repeat with aMatch in theMatches
	set theRange to (aMatch's rangeAtIndex:1) -- ie, $1
	set end of theResults to (aString's substringWithRange:theRange) as text
end repeat

It’s the sort of thing you can put in a library and forget.

JXA is Javascript Script Library? I tried this, but a you can only call a JavaScript script library from a JavaScript-script. It seems you can’t mix languages…

Hmm, looks like a lot of boilerplate.
A run shell script with awk or something will probably also do the trick.

It depends a bit on your priorities – shorter code or quicker execution.

No, “JXA” is JavaScript for Automation, which uses JavaScript as its core language.

Just like with AppleScript, any JXA script file with handlers (functions) can be a script library if you place it in one of the locations Apple has established.

See:

As I mentioned above, I have found through limited testing that you can use an AppleScript Script Library with JXA script, but not the other way around.

Shane Stanley:
Think NSRegularExpression.

Jim Underwood:
Are these as easy to use as JavaScript Regular Expressions?

This brings to mind an observation made by Jamie Zawinski, an American programmer and open source contributor:

Some people, when confronted with a problem, think "I know, I’ll use regular expressions."
Now they have two problems.

Sorry, I’m not sure I get your point.

Regular Expressions is fairly independent of the software in which it used (yes, there are differences, some significant).

My point is that the JavaScript implementation of RegEx is one of the easiest to use of any language I have used. In addition, there are many, many examples of using RegEx in JavaScript. I suspect NSRegularExpression is much less well known, thus fewer examples.

NSRegularExpression and JXA both use the same regular expression engine (ICU).

Many people don’t get Zawinski’s humor and actually take his statement seriously, as indicated by this post by Rex at http://www.rexegg.com/regex-humor.html in October of 2015:

I’ve been shocked at the number of people who didn’t get the joke and wrote in to explain the [two] so-called problems over the past year.

Of course, you’re not trying to explain the problems, you’re denying there are any. Despite the different manifestation, the cause may be the same—perhaps you don’t get the joke either. :wink:

Well Stan, perhaps I’m the only person out there who didn’t get your point, and could not care less about Zawinski’s joke. No doubt RegEx is a tough nut to crack, but my point was about ease of use, whatever the user’s/programmer’s knowledge is about RegEx.

Please don’t presume to read my mind and state what I’m trying to do, or not do. I am being very direct, very straight-forward. I find JavaScript RegEx easy to use, ASObjC, not so much. Others may disagree.

I think we all get that. But the OP asked about doing stuff from AppleScript, and said he has made an iPhone app, which suggests he’s probably familiar with NSRegularExpression already. So his perspective may well be quite different from yours (or mine).

I fully understand that (although the OP was also asking about JavaScript). I was just asking in general, as I was interested, and I suspect others might be as well.

There are a number of things that, IMO, it is easier/better to do in JavaScript than in AppleScript. It is a shame that AppleScript scripts can’t reference a JXA script library.

Did you try out the code I posted?

Hi all, thanks for the suggestions. I’m just learning the language, so trying thinks left and right. Now I think JavaScript is not the way to go. I ended up writing the use-case I had just in plain AppleScript, but I learned a lot on the way. Thanks.

Thank you Shane. that was really helpful. I frequently wish to use GREP but it has always been difficult to use in AppleScript. until now…

I expanded your code a little:

(*
Perform GREP searches using NSString:

To replace:
replaceString(inStr, findGREP, replaceGREP)
Returns the amended string

To find a single instance:
findString(inStr, findGREP)
Returns the found string.
If there are no matches, the constant null is returned.

To find multiple instances:
findStrings(inStr, findGREP)
Returns a list of the found strings
For each found match, a list is returned:
{$0, $1, $2, etc}
If a group is not found, the constant null is returned.
{$0, null, $2, etc}
If no matches are found, an empty list is returned.

The \ must be used to escape other \ and " characters within the GREP expressions.
*)

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

-- Most of the work is going to and from NSStrings.

-- A replace operation is simple:
-- sample GREP: "\\$([0-9,.]+)"
to replaceString(inStr, findGREP, replaceGREP)
	set aString to current application's NSString's stringWithString:inStr
	set aString to (aString's stringByReplacingOccurrencesOfString:findGREP withString:replaceGREP options:(current application's NSRegularExpressionSearch) range:{0, aString's |length|()}) as text
	return aString
end replaceString


-- Finds are a bit more complex in that the results are returned as ranges,
-- which you may then need to use to extract substrings:
-- sample GREP: "(?<=\\$)[0-9,.]+"
to findString(inStr, findGREP)
	set aString to current application's NSString's stringWithString:inStr
	set theRange to aString's rangeOfString:findGREP options:(current application's NSRegularExpressionSearch)
	if theRange's |length| is not 0 then
		return (aString's substringWithRange:theRange) as text
	else
		return null -- no matches
	end if
end findString


-- Or if there could be multiple matches:
to findStrings(inStr, findGREP)
	set aString to current application's NSString's stringWithString:inStr
	set {theExpr, theError} to current application's NSRegularExpression's regularExpressionWithPattern:findGREP options:0 |error|:(reference)
	set theMatches to theExpr's matchesInString:aString options:0 range:{0, aString's |length|()}
	set theResults to {}
	repeat with aMatch in theMatches
		set theseMatches to {}
		repeat with r from 0 to (((aMatch's numberOfRanges) as integer) - 1)
			set theRange to (aMatch's rangeAtIndex:r)
			if theRange's |length| is not 0 then
				set end of theseMatches to (aString's substringWithRange:theRange) as text
			else
				set end of theseMatches to null -- no match for this group
			end if
		end repeat
		set end of theResults to theseMatches
	end repeat
	return theResults
end findStrings

We had a similar discussion in Nov 2017, and Shane provided similar scripts. Here’s my refactor of Shane’s scripts, combining find of first match with find of all matches.

RegEx Handler to Return Capture Groups

(*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
@RegEx Handler to Get List of Capture Groups @ASObjC @Shane @AS
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
DATE:    2017-11-22
AUTHOR: JMichaelTX refactor of Handlers by ShaneStanley
REF:
  • Does SD6 Find RegEx Support Case Change?
    • Late Night Software Ltd., 
  • http://forum.latenightsw.com//t/does-sd6-find-regex-support-case-change/816/8

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*)
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

property LF : linefeed

set sourceStr to " 2016-01-11 and some text and  2017-02A-22"
set reFind to "(\\d{4})-(\\d{2})(A)?-(\\d{2})"

set reFirstMatch to my regexFind:reFind inString:sourceStr findAll:false
-->{"2016-01-11", "2016", "01", "", "11"}

set reAllMatches to my regexFind:reFind inString:sourceStr findAll:true
-->{{"2016-01-11", "2016", "01", "", "11"}, {"2017-02A-22", "2017", "02", "A", "22"}}

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on regexFind:pFindRegEx inString:pSourceString findAll:pGlobalBool
  -------------------------------------------------------------------
  
  (*
      Find Match(s) & Return Match with All Capture Groups as List of Lists
        • One List per Match
        • First Item is full match
        • Remaining Items are one item per Capture Group
            • IF CG not found, Item is returned as empty string
             
      if pGlobalBool is true, then Find ALL matches (Global) & return as list of lists
           else Find FIRST match, & return simple list of match & capture groups
      If NO matches, return empty list {}
      
      This handler is a merge and refactor of the two handlers provided by @ShaneStanley
       (http://forum.latenightsw.com//t/does-sd6-find-regex-support-case-change/816/8)
     
      All errors are mine.
     -------------------------------------------------------------------      
  *)
  
  local theFinds, theResult, subResult, groupCount
  
  try
    
    set pSourceString to current application's NSString's stringWithString:pSourceString
    set {theRegEx, theError} to current application's NSRegularExpression's regularExpressionWithPattern:pFindRegEx options:0 |error|:(reference)
    if theRegEx is missing value then error ("Invalid RegEx Pattern." & LF & theError's localizedDescription() as text)
    
    if (pGlobalBool) then ### FIND ALL MATCHES ###
      set theFinds to theRegEx's matchesInString:pSourceString options:0 range:{0, pSourceString's |length|()}
      
    else ### FIND FRIST MATCH ###
      set theFind to theRegEx's firstMatchInString:pSourceString options:0 range:{0, pSourceString's |length|()}
      set theFinds to current application's NSMutableArray's array()
      (theFinds's addObject:theFind)
    end if
    
    set theResult to current application's NSMutableArray's array()
    
    repeat with aFind in theFinds
      set subResult to current application's NSMutableArray's array()
      set groupCount to aFind's numberOfRanges()
      
      repeat with i from 0 to (groupCount - 1)
        
        set theRange to (aFind's rangeAtIndex:i)
        if |length| of theRange = 0 then
          --- Optional Capture Group was NOT Matched ---
          (subResult's addObject:"")
        else
          --- Capture Group was Matched ---
          (subResult's addObject:(pSourceString's substringWithRange:theRange))
        end if
      end repeat
      
      (theResult's addObject:subResult)
      
    end repeat -- theFinds
    
  on error errMsg number errNum
    set errMsg to "ASObjC RegEx ERROR #" & errNum & LF & errMsg
    set the clipboard to errMsg
    display dialog errMsg & LF & ¬
      "(error msg is on Clipboard)" with title (name of me) with icon stop
    error errMsg
    
  end try
  
  return theResult as list
end regexFind:inString:findAll: