Tuesday, November 19, 2019
  • Index
  •  » Code Exchange
  •  » Faster NSAppleScript alternative to AppleScript's "run script" command

#1 2019-11-01 02:49:54 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Faster NSAppleScript alternative to AppleScript's "run script" command

AppleScript's run script command is an extremely useful tool that allows the user to programatically run an AppleScript script in the form of a text string, text file, compiled script file, compiled script bundle, or application bundle. A potential downside of the run script command is the execution overhead time it incurs, typically between a couple of hundredths of a second in the best of cases to several tenths of a second in the worst. Although this time cost may not be important in many circumstances, it can become a prohibitive obstacle to the command's use, for example, in a large repeat loop.

The following handler, which utilizes Cocoa's NSAppleScript class to execute the AppleScript script, reproduces the behavior of the run script command but runs 10 to 15 times faster in my timing tests. Typical run times for very simple scripts have ranged from 1 to 2 thousandths of a second, which appears to be its execution overhead time. The handler accepts forms of script input similar to that of run script: a text string, or a reference to a text file, compiled script file, compiled script bundle, or application bundle. The reference to a script file or bundle may be in the form of a POSIX path, HFS path, or AppleScript alias. Error checking is performed at various steps, and an error is thrown to the calling program if one occurs. The result of script execution is returned to the calling program as a native AppleScript value by coercing the NSAppleEventDescriptor object returned by the NSAppleScript object execution.

Here are examples of handler usage:

Applescript:


-- Example of a text string script returning native AppleScript values

set scriptAsText to "return {1, 2.2, \"three\", true, current date, path to library folder from local domain, {1, 2, 3}, {aa:1, bb:2, cc:3}}"
return runScript(scriptAsText) --> the AppleScript record {1, 2.2, "three", true, date "Friday, November 1, 2019 at 3:35:36 PM", alias "Macintosh HD:Library:", {1, 2, 3}, {aa:1, bb:2, cc:3}}

-- Example of a script saved as a text file returning native AppleScript values

set scriptPath to ((path to desktop as text) & "SampleScript.applescript")'s POSIX path
do shell script "echo " & scriptAsText's quoted form & " >" & scriptPath's quoted form
return runScript(scriptPath) --> the AppleScript record {1, 2.2, "three", true, date "Friday, November 1, 2019 at 3:35:36 PM", alias "Macintosh HD:Library:", {1, 2, 3}, {aa:1, bb:2, cc:3}}

-- Example of a text string script returning a record of application object values

set scriptAsText to "tell application \"Finder\" to return (get properties of file 1 of desktop)"
return runScript(scriptAsText) --> the Finder record {name:..., index:..., displayed name:..., name extension:... }

Here is the handler:

Applescript:


use framework "Foundation"
use scripting additions

on runScript(theScript)
   -- The handler input argument is a reference to an AppleScript script in any of the following forms: text string, text file, compiled script file, compiled script bundle, or application bundle
   -- The handler return value is the AppleScript value returned by execution of the input script
   script util
       on processError(errorDict)
           set errorNumber to missing value
           try
               set errorNumber to (errorDict's NSAppleScriptErrorNumber) as integer
           end try
           if errorNumber ≠ missing value then
               set errorMessage to "missing value"
               try
                   set errorMessage to (errorDict's NSAppleScriptErrorMessage) as text
               end try
               if errorMessage = "missing value" then set errorMessage to "[No error message]"
               error ("Problem with handler runScript:" & return & return & errorMessage) number errorNumber
           end if
       end processError
   end script
   -- Test if the input argument is a file reference
   set scriptAlias to missing value
   try
       set scriptAlias to theScript as alias
   on error
       try
           set scriptAlias to theScript as POSIX file as alias
       end try
   end try
   -- Create an NSAppleScript object from the input argument
   if scriptAlias ≠ missing value then
       -- If the input argument is a file reference, try to create an NSAppleScript object from the file, or throw an error if one occurs
       set scriptURL to current application's |NSURL|'s fileURLWithPath:(scriptAlias's POSIX path)
       set {scriptObj, errorDict} to current application's NSAppleScript's alloc()'s initWithContentsOfURL:(scriptURL) |error|:(reference)
       if errorDict ≠ missing value then util's processError(errorDict as record)
   else
       -- If the input argument is not a file reference, try to create an NSAppleScript object directly from its text content, or throw an error if one occurs
       set scriptObj to current application's NSAppleScript's alloc()'s initWithSource:(theScript)
       if scriptObj = missing value then util's processError({NSAppleScriptErrorMessage:"The input argument neither is a reference to an existing file nor can it be formed into an executable AppleScript object.", NSAppleScriptErrorNumber:-2700})
   end if
   -- If an NSAppleScript object was successfully created, try to execute the script, or throw an error if one occurs
   set {returnValue, errorDict} to (scriptObj's executeAndReturnError:(reference)) as list
   if errorDict ≠ missing value then util's processError(errorDict as record)
   -- If the script executed successfully, coerce the returned NSAppleEventDescriptor object into its equivalent AppleScript value, and return the AppleScript value to the calling program
   tell returnValue's descriptorType() to set {isList, isRecord, isNothing} to {it = 1.818850164E+9, it = 1.919247215E+9, it = 1.853189228E+9}
   if isList then
       return returnValue as list
   else if isRecord then
       return returnValue as record
   else if isNothing then
       return
   else
       return (returnValue as list)'s first item
   end if
end runScript

Note:  To those who know more about NSAppleEventDescriptor-to-Applescript value coercion than I, may I ask if there is a better way to test for descriptor type than the numerical testing I'm using? I tried but failed to test against descriptor type enums, for instance typeAEList for a list, which for some reason weren't recognized during compilation. More broadly speaking, is there a better way to perform the coercion?

Last edited by bmose (2019-11-01 03:39:37 pm)

Offline

 

#2 2019-11-01 03:45:17 pm

CK
Member
From:: UK
Registered: 2018-11-04
Posts: 101

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

bmose wrote:

Note:  To those who know more about NSAppleEventDescriptor-to-Applescript value coercion than I, may I ask if there is a better way to test for descriptor type than the numerical testing I'm using? I tried but failed to test against descriptor type enums, for instance typeAEList for a list, which for some reason weren't recognized during compilation. More broadly speaking, is there a better way to perform the coercion?


You could do this:

Applescript:

(current application's NSAppleEventDescriptor's descriptorWithTypeCode:(returnValue's descriptorType()) as type class

which will give you the AppleScript type class explicitly, e.g. list, but that doesn't avoid the if...then...else affair.

You can get AppleScript to choose the most appropriate type class by doing:

Applescript:

return the returnValue as {list, record, number, text, anything}

but, unless you know you'll be dealing with specific types upon return and can avoid including anything in that list, it would sit a bit uneasy were it me (if you know your return type is either going to be a list or a record, say, then this works fine).

But I think Shane's trick of getting AppleScriptObjC to unwrap the contents of an array/list is probably the safest way to go:

Applescript:

item 1 of ((current application's NSArray's arrayWithObject:returnValue) as list)

Last edited by CK (2019-11-01 03:46:56 pm)

Offline

 

#3 2019-11-01 04:33:25 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

CK, thank you for the suggestions.

When I try the ...as type class approach, Script Debugger removes the final keyword class during compilation, and the method fails. Any idea what's going on with that?

I like Shane's item 1 of array approach. It does have one loophole, namely when the executed script returns nothing (for example, when the script quits with a return command that has no argument.) My numerical approach tests for that with: it = 1.853189228E+9  If I could somehow test for that outlier case corresponding to <NSAppleEventDescriptor: null()>, I could then implement the item 1 of array approach. Is there a way to test for that special case?

Thanks again for your help.

Offline

 

#4 2019-11-01 05:12:32 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

For what it's worth, I did find a way to test the NSAppleEventDescriptor return value for a value of "nothing" (other than testing its descriptorType for the numerical value 1.853189228E+9). The following appears to return true only when the descriptor = "nothing":

Applescript:


tell returnValue to set isNothing to (its |data|()'s |length|() = 0) and (its stringValue() = missing value)

Still, this seems like a rather roundabout way of getting at this value.

Offline

 

#5 2019-11-01 06:05:18 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6034

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Here's a variation of your code. It requires 10.11, and it is a little simpler. It handles the nullDescriptor case, and it does the error stuff inline for simplicity.

Applescript:

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use scripting additions

on runScript(theScript)
   set theScript to (current application's NSArray's arrayWithObject:theScript)'s firstObject()
   if (theScript's isKindOfClass:(current application's |NSURL|)) as boolean then
       set {scriptObj, errorDict} to current application's NSAppleScript's alloc()'s initWithContentsOfURL:(scriptURL) |error|:(reference)
       if errorDict is not missing value then
           set {theError, errorNumber} to theDict's objectsForKeys:{current application's NSAppleScriptErrorMessage, current application's NSAppleScriptErrorNumber} notFoundMarker:"[No error message]"
           error theError as text number errorNumber as integer
       end if
   else
       set scriptObj to current application's NSAppleScript's alloc()'s initWithSource:(theScript)
       if scriptObj = missing value then error "The input argument neither is a reference to an existing file nor can it be formed into an executable AppleScript object." number -2700
   end if
   set {returnValue, errorDict} to scriptObj's executeAndReturnError:(reference)
   if errorDict is not missing value then
       set {theError, errorNumber} to theDict's objectsForKeys:{current application's NSAppleScriptErrorMessage, current application's NSAppleScriptErrorNumber} notFoundMarker:"[No error message]"
       error theError as text number errorNumber as integer
   end if
   if (returnValue's isEqual:(current application's NSAppleEventDescriptor's nullDescriptor())) as boolean then return
   return item 1 of ((current application's NSArray's arrayWithObject:returnValue) as list)
end runScript

Speed-wise, it's a whisker faster using your sample script, but there's very little in it.

However...

My tests in Script Geek show that the speed of using run script is actually faster than both -- the ASObjC versions take about 25% longer with your sample code.

I wonder how you did your tests. If you did them in a script editor, that would explain the difference -- code involving Apple events always gets an inflated time because of the extra overhead of log instrumentation.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#6 2019-11-02 08:10:25 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Shane,

Thank you pointing out the null-descriptor test for no return value. That, combined with the [array -> list -> item 1] coercion method, is a nice compact way of converting an NSAppleEventDescriptor result to an AppleScript value. I find that it runs about 10% slower (give or take) than my original method of testing against numerical constants. Given that the numerical test executes a bit faster, is there a downside to using it? For example, how great is the risk that Apple might change the numerical constants at some arbitrary point in the future?

Regarding execution speed, here is a sample execution speed test:

Applescript:


set scriptAsText to "return {1, 2.2, \"three\", true, current date, path to library folder from local domain, {1, 2, 3}, {aa:1, bb:2, cc:3}}"

-- Each handler is executed once prior to beginning the timing tests in the hope that this might mitigate spurious effects on execution times due to caching.

set x1 to my runScript(scriptAsText)
set x2 to my runScriptNEW(scriptAsText)
set x3 to my |AppleScript's run script|(scriptAsText)

set nn to 100

set t1 to current application's CACurrentMediaTime()
repeat nn times
   set x1 to my runScript(scriptAsText)
end repeat
set t1 to ((current application's CACurrentMediaTime()) - t1) / nn

set t2 to current application's CACurrentMediaTime()
repeat nn times
   set x2 to my runScriptNEW(scriptAsText)
end repeat
set t2 to ((current application's CACurrentMediaTime()) - t2) / nn

set t3 to current application's CACurrentMediaTime()
repeat nn times
   set x3 to my |AppleScript's run script|(scriptAsText)
end repeat
set t3 to ((current application's CACurrentMediaTime()) - t3) / nn

tell application "System Events"
   activate
   display dialog "t1=" & t1 & return & "t2=" & t2 & return & "t3=" & t3 & return & "t2 / t1=" & t2 / t1 & return & "t3 / t1=" & t3 / t1 & return & "t3 / t2=" & t3 / t2
end tell

on runScript(theScript)

   # My originally submitted script, including the following (original) ending... #

   -- If the script executed successfully, coerce the returned NSAppleEventDescriptor object into its equivalent AppleScript value, and return the AppleScript value to the calling program
   tell returnValue's descriptorType() to set {isList, isRecord, isNothing} to {it = 1.818850164E+9, it = 1.919247215E+9, it = 1.853189228E+9}
   if isList then
       return returnValue as list
   else if isRecord then
       return returnValue as record
   else if isNothing then
       return
   else
       return (returnValue as list)'s first item
   end if
end runScript

on runScriptNEW(theScript)
   
   # My originally submitted script, except with the [null-descriptor] test and [array -> list -> item 1] coercion... #

   -- If the script executed successfully, coerce the returned NSAppleEventDescriptor object into its equivalent AppleScript value, and return the AppleScript value to the calling program
   if (returnValue's isEqual:(current application's NSAppleEventDescriptor's nullDescriptor())) as boolean then return
   return ((current application's NSArray's arrayWithObject:returnValue) as list)'s first item
end runScriptNEW

on |AppleScript's run script|(theScript)

   # Using AppleScript's [run script] command #

   return run script theScript
end executeRunScript

Here are typical results, reproduced on multiple timing tests. I don't find any discernible effect on execution times of changing the order in which the handlers are executed.

When the timing script is run as an applet via the Finder:

    t1=0.0018
    t2=0.0021
    t3=0.0197 (9 to 10x slower than t1 or t2)

When the timing script is run as a script via LaunchBar:

    t1=0.0020
    t2=0.0020
    t3=0.0227 (11x slower than t1 or t2)

When the timing script is run as a script via Script Debugger:

    t1=0.0017
    t2=0.0019
    t3=0.0211 (11 to 12x slower than t1 or t2)

In these tests, AppleScript's run script command reproducibly executes 9 to 12x slower than my script, and my script as originally submitted is a bit faster than that incorporating the [null-descriptor] test and [array -> list -> item 1] coercion.

Last edited by bmose (2019-11-02 08:18:19 am)

Offline

 

#7 2019-11-02 03:21:14 pm

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 5105

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

In Script Editor, I'm getting similar results to Shane's with regard to relative speeds, although they can vary with parameter scripts which take a long time to run. They're both interesting handlers, though. Thanks, fellas!  smile

CK wrote:

You can get AppleScript to choose the most appropriate type class by doing:

Applescript:

return the returnValue as {list, record, number, text, anything}


This is interesting too. I'd never seen it before. But with the classes in the order above, while NSArrays are returned as lists, other cocoa types are returned as AS values in lists. Changing the order of classes has different side effects, depending on the value being coerced. A simple 'as anything' works with all the cocoa and AS values I've tried so far, but it's not an official AS coercion!


NG

Offline

 

#8 2019-11-02 09:01:02 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6034

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

bmose wrote:

how great is the risk that Apple might change the numerical constants at some arbitrary point in the future?



My guess would be somewhat less than 1 in 1.853189228E+9. That's simply the integer value of the four-letter code 'null':

Applescript:

current application's NSHFSTypeCodeFromFileType("'null'")

Regarding execution speed



That's very interesting. I'm surprised at the results in an applet; they're less efficient when events are involved than I thought.

Last edited by Shane Stanley (2019-11-02 09:16:40 pm)


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#9 2019-11-02 09:16:01 pm

Shane Stanley
Member
From:: Australia
Registered: 2002-12-07
Posts: 6034

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Nigel Garvey wrote:

A simple 'as anything' works with all the cocoa and AS values I've tried so far



Applescript:

set x to current application's NSArray's arrayWithObject:{1}
x as anything --> 1

but it's not an official AS coercion



It's also a bit tricky. It's essentially the wildcard, «class ****», and in pre-OS X it used to be defined as any. Script Debugger defines any and anything as synonyms for backwards compatibility, and the compiler can choose a different version under differing (undefined) conditions.

Before third-party scripting additions died, there was also a wrinkle with Satimage.osax, which defined four terms with a value of '****': list of alias or list of string, list of string or string, list of real or real, and list of matchrecord, list of string, machrecord or string. You would get one of these if the script also had a use scripting additions statement, which meant the addition terminology was loaded first. Which one you got depended a bit on the version of Satimage.osax.


Shane Stanley <sstanley@myriad-com.com.au>
www.macosxautomation.com/applescript/apps/
latenightsw.com

Offline

 

#10 2019-11-02 10:22:56 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Shane Stanley wrote:

My guess would be somewhat less than 1 in 1.853189228E+9. That's simply the integer value of the four-letter code 'null'

Thanks for the great information. They say that death and taxes are the only two certainties in life.  Maybe we could add 1.853189228E+9 for the null value to that list!

Offline

 

#11 2019-11-03 02:49:28 pm

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Nigel Garvey wrote:

In Script Editor, I'm getting similar results to Shane's with regard to relative speeds

I too find that AppleScript's run script command executes a bit faster than my runScript handler when I run my timing script via Script Editor. On the other hand, my runScript handler runs about 10 to 15x faster than AppleScript's run script command in every other script runner I tested, including:

    - Script Debugger (as a script)
    - Finder (as an applet)
    - LaunchBar (as a script)
    - Terminal (as an osascript)
    - Automator (via the Run as AppleScript service)
    - Scripts menu on the right side of the menu bar (as a script)
    - Objective-C application (via the Finder, running the timing script as an NSAppleScript from the script text)
    - Objective-C application (via the Finder, running the timing script as an NSAppleScript from the compiled script)
    - Script Libraries script (as a handler in the utility script)

Not sure how to explain this discrepancy, but it would appear that Script Editor is somehow avoiding the ~0.02 second overhead cost of AppleScript's run script command present with all the other script runners.

P.S. To add another monkey wrench to the works, when I run the latter two Objective-C applications from within Xcode, AppleScript's run script command continues to incur the ~0.02 second overhead cost, but my runScript handler now incurs a ~0.01 second overhead cost not seen with any of the other script runners, including Script Editor. My runScript handler is still faster than AppleScript's run script command in this specific case, but now by about 2.5x rather than 10 to 15x.

Edit note: The Objective-C tests were added after I had originally submitted this post.

Edit note 2: The Script Libraries script result was also added after I had originally submitted this post.

Last edited by bmose (2019-11-08 10:27:02 am)

Offline

 

#12 2019-11-04 06:35:21 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Here is a slightly improved version of my originally submitted handler. It allows the input script to be in the form of a text string, or a POSIX path, HFS path, AppleScript alias, POSIX file («class furl»), or NSURL object (like Shane's script) pointing to a script text file, compiled script file, compiled script bundle, or application bundle. It also has minor improvements in error handling. The numerical comparison method of coercing the return value was retained simply because it executes the fastest.

Applescript:


use framework "Foundation"
use scripting additions

on runScript(theScript)
   -- The handler input argument is a reference to an AppleScript script in any of the following forms: text string, or a POSIX path, HFS path, AppleScript alias, POSIX file («class furl»), or NSURL object pointing to a script text file, compiled script file, compiled script bundle, or application bundle
   -- The handler return value is the AppleScript value returned by execution of the input script
   script util
       on processError(errorRecord)
           set errorNumber to missing value
           try
               set errorNumber to (errorRecord's NSAppleScriptErrorNumber) as integer
           end try
           if errorNumber = -128 then
               error number -128
           else if errorNumber ≠ missing value then
               set errorMessage to "missing value"
               try
                   set errorMessage to (errorRecord's NSAppleScriptErrorMessage) as text
               end try
               if errorMessage = "missing value" then set errorMessage to "[No error message]"
               if errorNumber ≠ -2700 then set errorMessage to "(" & errorNumber & ") " & errorMessage
               error ("Problem with handler runScript:" & return & return & errorMessage) number errorNumber
           end if
       end processError
   end script
   -- Test if the input argument is a file reference, and if so, form it into an NSURL object
   tell theScript
       set {scriptURL, isNonURLFileRef} to {missing value, true}
       try
           set scriptAlias to it as alias
       on error
           try
               set scriptAlias to it as POSIX file as alias
           on error
               set isNonURLFileRef to false
               try
                   if (its isKindOfClass:(current application's |NSURL|'s class)) then set scriptURL to it
               end try
           end try
       end try
       if isNonURLFileRef then set scriptURL to current application's |NSURL|'s fileURLWithPath:(scriptAlias's POSIX path)
   end tell
   -- Create an NSAppleScript object from the input argument
   if scriptURL ≠ missing value then
       -- If the input argument is a file reference, try to create an NSAppleScript object from the file, or throw an error if one occurs
       set {scriptObj, errorDict} to current application's NSAppleScript's alloc()'s initWithContentsOfURL:(scriptURL) |error|:(reference)
       if errorDict ≠ missing value then util's processError(get errorDict as record)
   else
       -- If the input argument is not a file reference, try to create an NSAppleScript object directly from its text content, or throw an error if one occurs
       set scriptObj to current application's NSAppleScript's alloc()'s initWithSource:(theScript)
       if scriptObj = missing value then util's processError({NSAppleScriptErrorMessage:"The input argument neither is a reference to an existing file nor can it be formed into an executable AppleScript object.", NSAppleScriptErrorNumber:-2700})
   end if
   -- If an NSAppleScript object was successfully created, try to execute the script, or throw an error if one occurs
   set {returnValue, errorDict} to (scriptObj's executeAndReturnError:(reference)) as list
   if errorDict ≠ missing value then util's processError(get errorDict as record)
   -- If the script executed successfully, coerce the returned NSAppleEventDescriptor object into its equivalent AppleScript value, and return the AppleScript value to the calling program
   tell returnValue's descriptorType() to set {isList, isRecord, isNothing} to {it = 1.818850164E+9, it = 1.919247215E+9, it = 1.853189228E+9}
   if isList then
       return returnValue as list
   else if isRecord then
       return returnValue as record
   else if isNothing then
       return
   else
       return (returnValue as list)'s first item
   end if
end runScript

Last edited by bmose (2019-11-04 08:44:58 am)

Offline

 

#13 2019-11-07 05:47:13 pm

CK
Member
From:: UK
Registered: 2018-11-04
Posts: 101

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

Nigel Garvey wrote:
CK wrote:

You can get AppleScript to choose the most appropriate type class by doing:

Applescript:

return the returnValue as {list, record, number, text, anything}


This is interesting too. I'd never seen it before. But with the classes in the order above, while NSArrays are returned as lists, other cocoa types are returned as AS values in lists. Changing the order of classes has different side effects, depending on the value being coerced. A simple 'as anything' works with all the cocoa and AS values I've tried so far, but it's not an official AS coercion!

Quite right.  I wrote that reply in a bit of a multi-tasking rush, and I think it popped into my head later when I was trying to sleep that the order of that class list, as I had printed it, wasn't the best order.  Namely, record should come before list always.  Coercions likely to throw errors are best featured early in the list; and valid coercions are usually better positioned after the true class.

Sometimes it's easier/necessary to split one list into two serial coercion lists, i.e. ...as {doh, ray, mee} as {fah, so}, depending what one's overall objective is.

But, in truth, I haven't discerned a system of rules to for ordering that universally applies to every subset (sublist) of type classes, so the final order requires experimenting with but is, in my opinion, typically worth it for code that's much cleaner/easier to read.  I wonder if there are any performance differentials compared to if...then...else.  When I get a working Mac, I shall have to test.

Offline

 

#14 2019-11-08 09:50:25 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 314

Re: Faster NSAppleScript alternative to AppleScript's "run script" command

CK wrote:

I haven't discerned a system of rules to for ordering that universally applies

I would like to implement your novel coercion method if a universal scheme could be developed that handled values of all AppleScript and non-AppleScript classes, which this general purpose script requires. I suspect that it would execute at least as fast as the if...then...else... technique I am currently using.

Shane Stanley wrote:

That's simply the integer value of the four-letter code 'null'

Shane's helpful elaboration of the meaning of the descriptor numerical values lends confidence to using the raw numbers for type testing for the sake of execution speed:

Applescript:


For an AppleScript list: current application's NSHFSTypeCodeFromFileType("'list'") = 1.818850164E+9
For an AppleScript record: current application's NSHFSTypeCodeFromFileType("'reco'") = 1.919247215E+9
For no AppleScript value: current application's NSHFSTypeCodeFromFileType("'null'") = 1.853189228E+9

(I examined the relative speed of testing against the raw numbers versus against the above NSHFSTypeCodeFromFileType return values. The cost of the latter was small, about 0.00003 seconds per conversion or about 0.0001 seconds overall in view of the three conversions, but since the raw-number method is a tiny bit faster, I figured I might as well stay with it.)

Apologies for the multiple posts of the script code, but I felt compelled to submit this further improved version that handles errors more effectively. It wraps the entire handler code in a try block so that all errors are captured. It implements the objectsForKeys:notFoundMarker: method for retrieving error dictionary values, as Shane demonstrated in his script. And it posts error messages that are more topic-specific. The comments were also tuned up a bit.

Applescript:


on runScript(theScript)
   -- The handler input argument is a reference to an AppleScript script in the form of a text string, or a POSIX path, HFS path, AppleScript alias, POSIX file («class furl»), or NSURL object pointing to a script text file (e.g., .applescript), compiled script file (.scpt), compiled script bundle (.scptd), or application bundle (.app)
   -- The handler returns the value returned by execution of the input script; the value may be an AppleScript value or an ASObjC or other application object
   script util
       property errorSubheader : missing value
   end script
   -- Wrap the code in a try block to capture any error that may occur
   try
       -- If the input argument is a file reference, form it into an NSURL object
       tell theScript
           set {scriptURL, isNonURLFileRef} to {missing value, true}
           try
               set scriptAlias to it as alias
           on error
               try
                   set scriptAlias to it as POSIX file as alias
               on error
                   set isNonURLFileRef to false
                   try
                       if (its isKindOfClass:((current application's |NSURL|)'s |class|())) then set scriptURL to it
                   end try
               end try
           end try
           if isNonURLFileRef then set scriptURL to (current application's |NSURL|)'s fileURLWithPath:(scriptAlias's POSIX path)
       end tell
       -- Create an NSAppleScript object from the input argument
       if scriptURL ≠ missing value then
           -- If the input argument is a file reference, try to create an NSAppleScript object from the file, or throw an error if one occurs
           set {scriptObj, errorDict} to (current application's NSAppleScript)'s alloc()'s initWithContentsOfURL:(scriptURL) |error|:(reference)
           if errorDict ≠ missing value then
               set util's errorSubheader to "Cannot form an executable NSAppleScript object from the input file reference due to the following error:"
               set {errorMessage, errorNumber} to (errorDict's objectsForKeys:{current application's NSAppleScriptErrorMessage, current application's NSAppleScriptErrorNumber} notFoundMarker:"missing value") as list
               if errorNumber = "missing value" then set {errorMessage, errorNumber} to {get errorDict's |description|() as text, -2700}
               error errorMessage number errorNumber
           end if
       else
           -- If the input argument is not a file reference, try to create an NSAppleScript object directly from its text content, or throw an error if one occurs
           set scriptObj to (current application's NSAppleScript)'s alloc()'s initWithSource:(theScript)
           if scriptObj = missing value then error "The input argument is neither a text string script nor a reference to an existing script file."
       end if
       -- If an NSAppleScript object was successfully created, try to execute the script, or throw an error if one occurs
       set util's errorSubheader to "AppleScript execution error:"
       set {returnValue, errorDict} to (scriptObj's executeAndReturnError:(reference)) as list
       if errorDict ≠ missing value then
           set {errorMessage, errorNumber} to (errorDict's objectsForKeys:{current application's NSAppleScriptErrorMessage, current application's NSAppleScriptErrorNumber} notFoundMarker:"missing value") as list
           if errorNumber = "missing value" then set {errorMessage, errorNumber} to {get errorDict's |description|() as text, -2700}
           error errorMessage number errorNumber
       end if
       -- If the script executed successfully, coerce the returned NSAppleEventDescriptor object into the original AppleScript value or ASObjC or other application object returned by the script execution, and return the coerced value to the calling program
       set util's errorSubheader to missing value
       tell returnValue's descriptorType() to set {isList, isRecord, isNothing} to {it = 1.818850164E+9, it = 1.919247215E+9, it = 1.853189228E+9}
       if isList then
           return returnValue as list
       else if isRecord then
           return returnValue as record
       else if isNothing then
           return
       else
           return (returnValue as list)'s first item
       end if
   on error errorMessage number errorNumber
       if errorNumber = -128 then error number -128
       if errorMessage = "missing value" then set errorMessage to "[No error message]"
       if errorNumber ≠ -2700 then set errorMessage to "(" & errorNumber & ") " & errorMessage
       if util's errorSubheader ≠ missing value then set errorMessage to util's errorSubheader & return & return & errorMessage
       error ("Problem with handler runScript:" & return & return & errorMessage) number errorNumber
   end try
end runScript

Last edited by bmose (2019-11-08 12:01:22 pm)

Offline

 
  • Index
  •  » Code Exchange
  •  » Faster NSAppleScript alternative to AppleScript's "run script" command

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)