Tuesday, December 10, 2019

#1 2017-11-08 11:51:13 am

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

How to save an Applescript script object created within code to a file

I cannot figure out how to save an Applescript script object created within code to a file.  Two Scripting Additions commands that work in Applescript don't seem to work in ApplescriptObjC, namely store script and open for access...write... (in the latter case, by inserting the script object into a list and saving the list to a file.) In a previous post (http://macscripter.net/viewtopic.php?id=36714), it was suggested to use OSAKit.framework's writeToURL:ofType:usingStorageOptions:error: method. I have been able to get that approach to work provided that the script already exists in a file. For example, the following succeeds:

Applescript:


use framework "Foundation"
use framework "OSAKit"

property || : current application

set inputUrl to (||'s |NSURL|)'s fileURLWithPath:"/path/to/old/script.scpt"
set outputUrl to (||'s |NSURL|)'s fileURLWithPath:"/path/to/new/script.scpt"
set {theScript, theError} to (||'s OSAScript)'s alloc()'s initWithContentsOfURL:inputUrl |error|:(reference)
if theError = missing value then
   theScript's writeToURL:outputUrl ofType:(||'s OSAStorageScriptType) usingStorageOptions:0 |error|:(reference) --> succeeds
end if

How can I use that approach when the script object is created within code? For example, the following fails:

Applescript:


use framework "Foundation"
use framework "OSAKit"

property ||:current application

script theScript
   property foo:"bar"
end script

set outputUrl to (||'s |NSURL|)'s fileURLWithPath:"/path/to/new/script.scpt"
theScript's writeToURL:outputUrl ofType:(||'s OSAStorageScriptType) usingStorageOptions:0 |error|:(reference) --> fails

Last edited by bmose (2017-11-08 12:05:29 pm)

Offline

 

#2 2017-11-09 09:29:59 pm

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

Re: How to save an Applescript script object created within code to a file

I found a solution. While the store script command does seem to be broken, the open for access...write... approach works. My problem was that I was specifying the file reference as an hfs path. Changing the file reference to a posix path or posix file solved the problem.

To save the script object to a file, save it as a list item.

Applescript:

use framework "Foundation"
use scripting additions

property || : current application

-- The inline script object to be saved to a file
script theScript
   property foo : null
end script

-- Assign the script property a value
set theScript's foo to ||'s NSString's stringWithString:"bar"

-- Save the script object to a file as a list item
set fileRef to "/path/to/target/file.scpt" -- or "/path/to/target/file.scpt" as POSIX file
try
   close access fileRef
end try
set fid to open for access fileRef with write permission
set eof fid to 0
write {theScript} as list to fid
close access fid

To retrieve the saved script object, read the file as a list, and get the list item that is the script object. Be sure to use any needed frameworks.

Applescript:

use framework "Foundation"
use scripting additions

-- Retrieve the script object from the file
set fileRef to "/path/to/target/file.scpt" -- or "/path/to/target/file.scpt" as POSIX file
set theScript to (read fileRef as list from 1)'s item 1

-- Confirm that the script was properly retrieved
set theScript's foo to theScript's foo's uppercaseString()
return theScript's foo as text --> "BAR"

Also, see here for ways to get ASObjC code to run in instantiated script objects: http://www.macscripter.net/viewtopic.php?id=45913

Last edited by bmose (2017-11-09 09:31:04 pm)

Offline

 

#3 2017-11-10 06:17:52 am

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

Re: How to save an Applescript script object created within code to a file

bmose wrote:

While the store script command does seem to be broken, the open for access...write... approach works.


store script does appear to be broken when it's in the same script (object) as use framework "Foundation". It's possible to use it if the ASObjC stuff is moved to another script object in a handler below where store script is used:

Applescript:

-- The inline script object to be saved to a file
script theScript
   property foo : missing value
end script

set theScript's foo to getValue()
store script theScript in file ((path to desktop as text) & "Script.scpt")


on getValue()
   script ASObjC
       use framework "Foundation"
       
       on value()
           return "bar" --current application's NSString's stringWithString:"bar"
       end value
   end script
   
   return ASObjC's value()
end getValue

But then you hit a more fundamental problem in that you want to save a script containing an ASObjC pointer — and that officially can't be done. It would be a waste of time anyway, because it's unlikely the object pointed to would be would still be there when the pointer was eventually retrieved from the saved script.

This in turn casts doubt on your File Read/Write solution. (And giving a file containing a representation of a list the extension ".scpt" isn't recommended.) When I try it, an error occurs on the attempt to read the file as list.

Last edited by Nigel Garvey (2017-11-10 06:19:38 am)


NG

Offline

 

#4 2017-11-10 05:08:58 pm

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

Re: How to save an Applescript script object created within code to a file

Nigel Garvey wrote:

And giving a file containing a representation of a list the extension ".scpt" isn't recommended.

My bad.  I didn't mean to include a ".scpt" extension.

Nigel Garvey wrote:

But then you hit a more fundamental problem in that you want to save a script containing an ASObjC pointer — and that officially can't be done.

I understand your point and modified my effort to the more modest goal of saving a plain vanilla Applescript script object to a file from a script containing ASObjC code.  This is where things got weird. If I saved the plain vanilla script object to a file as a list item, then retrieved the script object by reading the list back from the file within the same script, I was able to retrieve the script object successfully:

Applescript:

use framework "Foundation"
use scripting additions

property || : current application

-- The inline plain vanilla Applescript script object to be saved to a file
script theScript
   property foo : null
end script

-- Assign the script property a value
set theScript's foo to (||'s NSString's stringWithString:"bar")'s uppercaseString() as text

-- Save the script object to a file as a list item
set fileRef to "/path/to/target/file"
try
   close access fileRef
end try
set fid to open for access fileRef with write permission
set eof fid to 0
write {theScript} as list to fid
close access fid

-- Retrieve the script object from the file within the same script
set fileRef to "/path/to/target/file"
set theScript to (read fileRef as list from 1)'s item 1

-- Confirm that the script was properly retrieved
return theScript's foo --> "BAR" --> Success!!

However, when I tried to retrieve the script object by reading the list back from the file from a different script, the retrieval failed with error -1761, which is Open Scripting Architecture error "There is a component mismatch—parameters are from two different components."

Applescript:

use framework "Foundation"
use scripting additions

property || : current application

-- The inline plain vanilla Applescript script object to be saved to a file
script theScript
   property foo : null
end script

-- Assign the script property a value
set theScript's foo to (||'s NSString's stringWithString:"bar")'s uppercaseString() as text

-- Save the script object to a file as a list item
set fileRef to "/path/to/target/file"
try
   close access fileRef
end try
set fid to open for access fileRef with write permission
set eof fid to 0
write {theScript} as list to fid
close access fid

Attempt to retrieve the script object from a different script:

Applescript:

use framework "Foundation"
use scripting additions

property || : current application

-- Retrieve the script object from the file from a different script
set fileRef to "/path/to/target/file"
set theScript to (read fileRef as list from 1)'s item 1 --> Failed with error -1761

-- Confirm that the script was properly retrieved
return theScript's foo

Any thoughts as to why the retrieval succeeds when performed within the original script but fails in a different script?

Last edited by bmose (2017-11-10 05:18:38 pm)

Offline

 

#5 2017-11-10 06:12:18 pm

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

Re: How to save an Applescript script object created within code to a file

Part of the normal process of saving a script involves flattening, or serializing, the contents, which basically takes a nested structure and its context and converts it into a string of bytes. The process is reversed when reading. (See AEFlattenDesc()).

I'm guessing this flattening isn't happening with your example. It doesn't matter when you read it back into the same script, but it obviously does if you read it elsewhere. And the ASObjC connection is the likely cause.

I think this is what's known technically as a dead horse. It's probably time to stop flogging it wink

But I'm curious about why you were trying it in the first place.


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

Offline

 

#6 2017-11-10 11:23:17 pm

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

Re: How to save an Applescript script object created within code to a file

Shane Stanley wrote:

And the ASObjC connection is the likely cause.

It is my understanding that store script has been broken for years in ASObjC. Apple hasn't deprecated store script in plain vanilla Applescript, so they must consider it to be of some value. So I'm wondering why they haven't fixed this. I assume bug reports have been filed, so does that mean that (A) they don't consider it a high priority, (B) it is unfixable because of the way ASObjC is implemented, or (C) some other reason?

Shane Stanley wrote:

But I'm curious about why you were trying it in the first place.

I'm working on a project which, if it were available, would be nicely solved with a script object capable of retaining state between invocations and supplied with handlers incorporating the power of ASObjC code. Unfortunately, because of the aforementioned problem with script objects in ASObjC, I have instead taken the approach of using two scripts. One is a plain vanilla Applescript script object that is instantiated for the current task at hand and that retains state between invocations. Its plain vanilla handlers serve only as front ends for the ASObjcC handlers in the second script. The second script is a script file acting essentially as a singleton and consisting only of the ASObjC handlers that the plain vanilla script object calls. I realize that there are other ways to retain state, but a script object that could both retain state and be coded with ASObjC handlers would be a very nice tool in the toolbox.

Last edited by bmose (2017-11-10 11:24:46 pm)

Offline

 

#7 2017-11-11 12:16:06 am

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

Re: How to save an Applescript script object created within code to a file

bmose wrote:

It is my understanding that store script has been broken for years in ASObjC.



I don't think it's ever worked there.

I assume bug reports have been filed



I'm not sure you can assume this -- scripters are notoriously poor at logging bugs.

so does that mean that (A) they don't consider it a high priority, (B) it is unfixable because of the way ASObjC is implemented, or (C) some other reason?



I wouldn't like to guess -- assuming bugs have been filed -- but I certainly wouldn't be surprised if it's A or B.


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

Offline

 

#8 2017-11-11 05:35:49 am

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

Re: How to save an Applescript script object created within code to a file

bmose wrote:

Unfortunately, because of the aforementioned problem with script objects in ASObjC …


The problems, as far as I can see, are just that store script doesn't work within the "scope" of a use framework … command and that you can't save scripts containing stored ASObjC values. store script is perfectly capable of storing scripts containing ASObjC code.

I'm not sure exactly what you want, but these three demos save viable script applications to your desktop which use an ASObjC method and store the AS equivalent result in a property:

Applescript:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

-- Script object instantiated at run time (no label after 'script').
-- Inherits ASObjC code from the compiled script object below it.
script
   property parent : script2
   property lastResult : missing value
   
   if (lastResult is missing value) then
       display dialog "First run"
   else
       display dialog "The previous result was " & lastResult
   end if
   
   set lastResult to |uppercase|("Hello!")
   display dialog "This result is " & lastResult
end script
set script1 to result

store script script1 in (((path to desktop as text) & "Script 1.app") as «class furl») replacing yes

-- Script object compiled into main script, below the 'store script' instruction to preserve the readability of the OSAX keywords.
-- The scope of 'use framework "Foundation"' is confined to this script object.
script script2
   use framework "Foundation"
   
   on |uppercase|(aText)
       return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
   end |uppercase|
end script

Applescript:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

store script myScript in (((path to desktop as text) & "My Script.app") as «class furl») replacing yes

-- Script object compiled into main script, below the 'store script' instruction to preserve the readability of the OSAX keywords.
-- The scope of 'use framework "Foundation"' is confined to this script object.
script myScript
   use framework "Foundation"
   property lastResult : missing value
   
   on |uppercase|(aText)
       return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
   end |uppercase|
   
   if (lastResult is missing value) then
       display dialog "First run"
   else
       display dialog "The previous result was " & lastResult
   end if
   
   set lastResult to |uppercase|("Hello!")
   display dialog "This result is " & lastResult
end script

Applescript:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

store script makeScript() in (((path to desktop as text) & "My Script.app") as «class furl») replacing yes

-- Script object instantiated at run time, below the 'store script' instruction to preserve the readability of the OSAX keywords.
-- The scope of 'use framework "Foundation"' is confined to this script object.
on makeScript()
   script
       use framework "Foundation"
       property lastResult : missing value
       
       on |uppercase|(aText)
           return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
       end |uppercase|
       
       if (lastResult is missing value) then
           display dialog "First run"
       else
           display dialog "The previous result was " & lastResult
       end if
       
       set lastResult to |uppercase|("Hello!")
       display dialog "This result is " & lastResult
   end script
   
   return result
end makeScript


NG

Offline

 

#9 2017-11-12 06:09:49 am

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

Re: How to save an Applescript script object created within code to a file

Shane Stanley wrote:

… scripters are notoriously poor at logging bugs.


Scripters generally find it more immediately useful to work round bugs than to complain about them and then sit around waiting for them to be fixed (and for clients to adopt the fixed systems).  wink

The only bug I've ever reported was the annoying Script Editor toolbar button problem. That was on 21st October 2014, back in the days of Yosemite and Script Editor 2.7. I had to go through the palava of signing up as a developer, reading through the spiel on how to file a bug report, and then describing the bug in several different ways in the report form. The response from Apple was b****r all for nearly a year, despite my updating the report twice during that time to note the bug's persistence through a Yosemite update and into El Capitan. Eventually Shane filed a report on it too and almost immediately (7th October 2015) I received an e-mail from Apple Developer Relations saying that Engineering had determined my bug report was a duplicate of another issue and would be closed (presumably meaning the report would be closed, not Engineering). It's now 12th November 2017, Script Editor's up to version 2.10 in High Sierra, and the bug still hasn't been fixed. If other scripters receive similar responses, it's not surprising they don't bother.


NG

Offline

 

#10 2017-11-12 04:49:39 pm

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

Re: How to save an Applescript script object created within code to a file

Nigel Garvey wrote:

The only bug I've ever reported



My choice of the word "poor" was itself poor -- I meant in the sense of something they don't generally do.

Not all bugs get fixed, obviously. But they do get logged, and importantly, the numbers are seen by managers who make the ultimate decisions. I remember Jon Pugh (of Jon's Additions fame) remarking that the single most valuable thing a scripter could do in terms of promoting it at Apple was to post bug reports.

Or as another engineer pointed out: some logged bugs don't get fixed, but no unlogged ones do.


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

Offline

 

#11 2017-11-17 07:59:36 am

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

Re: How to save an Applescript script object created within code to a file

Sorry for the delay in responding.

Nigel Garvey wrote:

these three demos save viable script applications to your desktop which use an ASObjC method and store the AS equivalent result in a property

Fantastic, Nigel, they work! Thanks for the solutions. The key is to confine the use framework "Foundation" statement to a script object embedded within the script object to be saved.

Shane Stanley wrote:

the single most valuable thing a scripter could do in terms of promoting it at Apple was to post bug reports

I submitted the store script/use framework "Foundation" bug today. I also had previously submitted an Applescript memory management bug in ASObjC that cropped up in the early days of ARC in OS X 10.8 Mountain Lion (http://www.macscripter.net/viewtopic.php?pid=166877). When OS X 10.9 Mavericks came out less than a year later, the problem was resolved. I don't know if the bug report had anything to do with it, but it was great to see that the problem had been fixed.

Last edited by bmose (2017-11-17 09:05:56 am)

Offline

 

#12 2018-06-11 10:52:49 pm

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

Re: How to save an Applescript script object created within code to a file

As Nigel Garvey pointed out earlier in this discussion, a script object containing ASObjC code may be saved to disk with the store script scripting additions command provided the use framework "Foundation" statement's scope is confined to the script object and not to the top-level script as a whole. This is illustrated in the following script, which is adapted from one of Nigel's examples. As Nigel points out, the script object must be coded below the store script command in order to preserve the readability of OSAX keywords in the script object (in the current example, the display dialog command):

Applescript:


use scripting additions

set fileRef to ((path to desktop as text) & "MyScript.scpt") as «class furl»

store script myScript in fileRef with replacing

(load script fileRef)'s displayUppercase("foobar") --> displays "FOOBAR"

script myScript
   use framework "Foundation"
   
   on makeUppercase(aText)
       return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
   end makeUppercase
   
   on displayUppercase(aText)
       display dialog (my makeUppercase(aText))
   end displayUppercase
end script

The following is an alternative solution that allows one to code the script object above the store script command, if that is desired for any reason. The two required modifications are (1) to move the use scripting additions command to the script object, and (2) to wrap any scripting additions commands in the top-level script within a using terms from scripting additions block. The using terms from scripting additions block provides a workaround to the restriction that only one use scripting additions command is allowed in the code; it also prevents scripting additions commands from appearing in raw chevron syntax (and sometimes executing successfully on my machine, sometimes not, in true quirky AppleScript fashion):

Applescript:


script myScript
   use framework "Foundation"
   use scripting additions
   
   on makeUppercase(aText)
       return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
   end makeUppercase
   
   on displayUppercase(aText)
       display dialog (my makeUppercase(aText))
   end displayUppercase
end script

using terms from scripting additions
   
   set fileRef to ((path to desktop as text) & "MyScript.scpt") as «class furl»
   
   store script myScript in fileRef with replacing
   
   (load script fileRef)'s displayUppercase("foobar") --> displays "FOOBAR"
   
end using terms from

Addendum:

Even simpler, just pull the use scripting additions command out of the script object and place it at the top of the top-level script. That makes the using terms from scripting additions statement unnecessary:

Applescript:


use scripting additions

script myScript
   use framework "Foundation"
   
   on makeUppercase(aText)
       return (current application's NSString's stringWithString:aText)'s uppercaseString() as text
   end makeUppercase
   
   on displayUppercase(aText)
       display dialog (my makeUppercase(aText))
   end displayUppercase
end script

set fileRef to ((path to desktop as text) & "MyScript.scpt") as «class furl»

store script myScript in fileRef with replacing

(load script fileRef)'s displayUppercase("foobar") --> displays "FOOBAR"

This gets back to Nigel's original example, although there doesn't appear to be a requirement for the script object to be coded below the store script command.

Last edited by bmose (2018-06-12 06:20:25 am)

Offline

 

#13 2019-11-22 09:08:02 pm

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

Re: How to save an Applescript script object created within code to a file

(Note: I submitted the problem described in this post to Apple's Bug Reporter two years ago, but alas, the problem persists in macOS Catalina.)

I'm resurrecting this old discussion to propose a solution to the vexing problem I raised in this post a couple of years ago.

To re-state the original problem, AppleScript's store script command is broken when it is executed within the scope of an ASObjC use framework "Foundation" command. As a result, there is simply no way that I am aware of to save a compiled script object created within code to a disk file via a store script command within the scope of a use framework "Foundation" command. In the posts above, Nigel Garvey showed various ways around this obstacle by isolating the use framework "Foundation" command to a script object separate from the one in which the store script command is located, while keeping both script objects in the same parent script. That approach would require a significant restructuring of code in the common scenario where one would normally place the use framework "Foundation" command at the top of the script as a whole.

An alternative solution (that took me two years to figure out) is to outsource AppleScript's store script command to a handler in an external script that does not have an encompassing use framework "Foundation" command. One option would be to save the external script with its store script-containing handler to an arbitrary location on disk and access the script and its handler via a load script command. Another, and even more useful, option would be to save the external script with its store script-containing handler as a .scptd script bundle in the ~/Library/Script Libraries/ folder and access it in a standard fashion for ~/Library/Script Libraries/ script bundle handlers. The latter approach will be taken in the example below. (Note that ~/Library/Script Libraries/ is shorthand for [startup disk name]/Users/[home folder name]/Library/Script Libraries/.)

The proposed handler, named storeScript, is an implementation of such a store script-containing handler. The storeScript handler should be located in an external script file saved to disk. The external script must not have a use framework "Foundation" command at its top level. (That, after all, is what we are trying to avoid!)

The following code demonstrates the use of the storeScript handler to save a compiled script object located within the scope of a use framework "Foundation" command to a disk file. In preparation for the storeScript handler's use, save a script containing the storeScript handler as a script bundle named NonASOCLib.scptd in the ~/Library/Script Libraries/ folder, and make sure that NonASOCLib.scptd does not have a use framework "Foundation" command at the top level. Then run the following demonstration script:

Applescript:


-- Before executing the following code, save a script containing the storeScript handler as a script bundle named NonASOCLib.scptd in the ~/Library/Script Libraries/ folder, and make sure that NonASOCLib.scptd does not have a <use framework "Foundation"> command at the top level.

use framework "Foundation"
use scripting additions

-- This is the sample compiled script object created within code to be saved to disk
script FooBar
   property foo : "bar"
end script

-- This is the destination file where the script object is to be saved
set destinationFilePath to (path to desktop)'s POSIX path & "MyFooBarScript.scpt"

-- The following command would fail to save the compiled script object to disk because the <store script> command is in the scope of a <use framework "Foundation"> command
-- store script FooBar in destinationFilePath replacing yes

-- The following handler call succeeds in saving the compiled script object to disk because the handler's <store script> command is outside the scope of a <use framework "Foundation"> command
script "NonASOCLib"'s storeScript({compiledScript:FooBar, destinationFilePath:destinationFilePath, replaceExisting:"yes"})

-- This shows that the saved script object can be successfully retrieved and used
set savedScriptObject to (load script destinationFilePath)
display dialog savedScriptObject's foo --> displays "bar" in a dialog window

Here is the storeScript handler:

Applescript:


on storeScript(inputRecord)
   (** Saves a compiled Applescript script object to a file, whether or not the script object is within the scope of a <use framework "Foundation"> command **)
   -- Wrap the code in a considering block to prevent spurious behavior, and a try block to capture any errors
   considering diacriticals, hyphens, punctuation and white space but ignoring case and numeric strings
       try
           -- Extract the input record properties
           try
               tell inputRecord
                   if its class ≠ record then error
                   tell (it & {destinationFilePath:missing value, replaceExisting:"ask"})
                       if length ≠ 3 then error
                       set {compiledScript, destinationFilePath, replaceExisting} to {its compiledScript, its destinationFilePath, its replaceExisting}
                   end tell
               end tell
           on error
               error "The handler input argument must be a record with the following properties:" & return & return & tab & "compiledScript" & return & tab & tab & "(required)" & return & tab & "destinationFilePath" & return & tab & tab & "(optional; default value if omitted = missing value)" & return & tab & "replaceExisting" & return & tab & tab & "(optional; default value if omitted = \"ask\")"
           end try
           -- Process the input record properties
           if compiledScript's class ≠ script then error "The input argument compiledScript must be a compiled Applescript script."
           tell destinationFilePath
               if it = "tmp" then
                   tell me to set destinationFilePath to do shell script "echo \"/tmp/$(uuidgen).scpt\""
               else if it = "temp" then
                   tell me to set destinationFilePath to do shell script "echo \"" & (path to temporary items)'s POSIX path & "$(uuidgen).scpt\""
               else if it ≠ missing value then
                   if (its class ≠ text) or (it does not start with "/") or (it contains ":") then error "The input argument destinationFilePath is not a valid POSIX path."
                   if (it does not end with ".scpt") and (it does not end with ".scptd") and (it does not end with ".app") and (it does not end with ".applescript") then error "The input argument destinationFilePath must have one of the following file extensions:" & return & return & tab & ".scpt" & return & tab & ".scptd" & return & tab & ".app" & return & tab & ".applescript"
                   if (it ends with "/.scpt") or (it ends with "/.scptd") or (it ends with "/.app") or (it ends with "/.applescript") then error "The input argument destinationFilePath is missing a file name."
               end if
           end tell
           if (destinationFilePath ≠ missing value) and ({replaceExisting} is not in {"yes", "no", "ask"}) then error "The input argument replaceExisting must be one of the following text strings:" & return & return & tab & "\"yes\"" & return & tab & "\"no\"" & return & tab & "\"ask\""
           -- Save the compiled script to the destination file
           try
               set {scriptSaved, savedFilePath} to {false, missing value}
               if destinationFilePath = missing value then
                   store script compiledScript -- the user will be prompted for a destination file
               else
                   if replaceExisting = "yes" then
                       store script compiledScript in destinationFilePath replacing yes
                       set savedFilePath to destinationFilePath
                   else if replaceExisting = "no" then
                       store script compiledScript in destinationFilePath replacing no
                       set savedFilePath to destinationFilePath
                   else if replaceExisting = "ask" then
                       try
                           set preexistingFileFound to false
                           destinationFilePath as POSIX file as alias
                           set preexistingFileFound to true
                       end try
                       store script compiledScript in destinationFilePath replacing ask
                       if not preexistingFileFound then set savedFilePath to destinationFilePath
                   end if
               end if
               set scriptSaved to true
           on error m number n
               -- Catch an error if one occurs
               if n = -128 then -- if the user declined to save the script to a file
                   -- Do nothing; the handler will exit with baseline return values (scriptSaved = false, savedFilePath = missing value)
               else -- if an execution error occurred
                   if replaceExisting = "no" then
                       set m to "The script was not saved because the input argument replaceExisting = \"no\", and the following error occurred:" & return & return & tab & "(" & n & ") " & m
                   else
                       set m to "The script was not saved for the following reason:" & return & return & tab & "(" & n & ") " & m
                   end if
                   error m
               end if
           end try
           -- Return the result
           return {scriptSaved:scriptSaved, savedFilePath:savedFilePath}
       on error m number n
           if n = -128 then error number -128
           if n ≠ -2700 then set m to "(" & n & ") " & m
           error ("Problem with handler storeScript:" & return & return & m) number n
       end try
   end considering
end storeScript

Here are usage notes for the handler:

Applescript:


(*
USAGE:
   If the handler is saved in a script bundle in the ~/Library/Script Libraries/ folder:
       script "[script bundle file name without the .scptd file extension]"'s storeScript({compiledScript:..., destinationFilePath:..., replaceExisting:...}) --> returns {scriptSaved:..., savedFilePath:...}

   If the handler is saved in a script file elsewhere:
       (load script "[/POSIX/path/to/script/file/including/file/extension]")'s storeScript({compiledScript:..., destinationFilePath:..., replaceExisting:...}) --> returns {scriptSaved:..., savedFilePath:...}

INPUT:
   record of the form {compiledScript:..., destinationFilePath:..., replaceExisting:...}

       compiledScript = compiled Applescript script object

       destinationFilePath = POSIX path of the file where the input script should be saved
           - The file extension of the supplied path must be one of the following:
               .scpt        ->    the script will be saved as a compiled Applescript script
               .scptd        ->    the script will be saved as a compiled Applescript script bundle
               .app        ->    the script will be saved as an Applescript application bundle
               .applescript    ->    the script will be saved as an Applescript text file
       -or-
       destinationFilePath = "tmp"
           -> destinationFilePath will be set to the POSIX path of a randomly named unique temporary file of the form:
               "/tmp/[uuid].scpt"
               e.g.: "/tmp/904E688D-1AE9-4054-8B5A-A1731DBA4038.scpt"
       -or-
       destinationFilePath = "temp"
           -> destinationFilePath will be set to the POSIX path of a randomly named unique temporary file of the form:
               "/[path to temporary items folder]/[uuid].scpt"
               e.g.: "/private/var/folders/1s/_cphnwkx08x8wd_tv0zz6zq00000gn/T/TemporaryItems/904E688D-1AE9-4054-8B5A-A1731DBA4038.scpt"
       -or-
       * destinationFilePath = missing value
           - The user will be prompted for the file where the script is to be saved

       replaceExisting = "yes"
           - If the destination file already exists, it will be overwritten
       -or-
       replaceExisting = "no"
           - If the destination file already exists, it will not be overwritten, and an error will be thrown
       -or-
       * replaceExisting = "ask"
           - If the destination file already exists, the user will be prompted whether or not to overwrite it
       NOTE:
           - The replaceExisting argument is ignored if destinationFilePath = missing value, in which case
               the user will be prompted for a destination file
   
       * = default value if the argument is missing

OUTPUT:
   record of the form {scriptSaved:..., savedFilePath:...}

       scriptSaved
           true, if the input script was successfully saved to a file
           -or-
           false, if the input script was not successfully saved to a file

       savedFilePath
           POSIX path of the saved script file, provided that all of the following conditions are met:
               (1) The input script was successfully saved to a file
               -and-
               (2) A path was supplied in the destinationFilePath input argument (i.e., destinationFilePath ≠ missing value)
               -and-
               (3) The replaceExisting input argument = "yes" or "no"
                   -or-
                   The replaceExisting input argument = "ask", and no pre-existing file was found at the destinationFilePath location
           -or-
           missing value, provided that any of the following conditions is met:
               (1) The input script was not successfully saved to a file
               -or-
               (2) A path was not supplied in the destinationFilePath input argument (i.e., destinationFilePath = missing value)
               -or-
               (3) The replaceExisting input argument = "ask", and a pre-existing file was found at the destinationFilePath location

       NOTE:
           - No result will be returned and an error will be thrown instead, provided that all of the following conditions are met:
               (1) A path was supplied in the destinationFilePath input argument (i.e., destinationFilePath ≠ missing value)
               -and-
               (2) The replaceExisting input argument = "no"
               -and-
               (3) A pre-existing file was found at the destinationFilePath location
*)

Edit note: A minor improvement was made to the original posted version of the storeScript handler. Specifically, in the case where a path was specified in the destinationFilePath input argument (i.e., not missing value), the replaceExisting input argument = "ask", and no pre-existing file was found at the destinationFilePath location, the savedFilePath output argument will now contain the destinationFilePath value rather than the missing value.

Last edited by bmose (2019-11-23 01:20:53 pm)

Offline

 

#14 2019-12-05 08:13:33 am

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

Re: How to save an Applescript script object created within code to a file

The following is a condensed version of the storeScript handler from the previous post that does not have to be saved in a permanent external script file. Instead, the current handler may be incorporated inline in any ASObjC script containing a use framework "Foundation" command at the top level. In contrast with the previous version of the handler, the current handler does not perform input argument checking nor does it supply default values for missing input record properties in order to reduce the amount of code needed. Please refer to the previous "usage notes" for a description of the handler's input and output record properties.

The handler does the following:

(1) Takes as its input argument the same input record as described in the previously posted "usage notes", except that no input argument checking is performed, and all input record properties must be supplied with values (i.e., no default values will be supplied for missing properties.)
(2) Dynamically creates a temporary script file in the /tmp folder, which (A) does not have a use framework "Foundation" command, and (B) contains a condensed version of the storeScript handler.
(3) Executes the temporary script's script-storing handler, which executes AppleScript's store script command outside the scope of a use framework "Foundation" command.
(4) After a sufficient delay (i.e., 1 minute), deletes the temporary script file via a background process so that handler execution is not delayed. (Sorry, I'm being a neat freak here.)
(5) Returns the results of the store script execution as an output record as described in the previously posted "usage notes".

Applescript:


on storeScript({compiledScript:compiledScript, destinationFilePath:destinationFilePath, replaceExisting:replaceExisting})
   set tempScript to "
       on storeScriptTemp(compiledScript, destinationFilePath, replaceExisting)
           considering diacriticals, hyphens, punctuation and white space but ignoring case and numeric strings
               try
                   set {scriptSaved, savedFilePath} to {false, missing value}
                   if destinationFilePath = missing value then
                       store script compiledScript -- the user will be prompted for a destination file
                   else
                       if replaceExisting = \"yes\" then
                           store script compiledScript in destinationFilePath replacing yes
                           set savedFilePath to destinationFilePath
                       else if replaceExisting = \"no\" then
                           store script compiledScript in destinationFilePath replacing no
                           set savedFilePath to destinationFilePath
                       else if replaceExisting = \"ask\" then
                           try
                               set preexistingFileFound to false
                               destinationFilePath as POSIX file as alias
                               set preexistingFileFound to true
                           end try
                           store script compiledScript in destinationFilePath replacing ask
                           if not preexistingFileFound then set savedFilePath to destinationFilePath
                       end if
                   end if
                   set scriptSaved to true
                   return {scriptSaved:scriptSaved, savedFilePath:savedFilePath}
               on error m number n
                   if n ≠ -128 then
                       if replaceExisting = \"no\" then
                           set m to \"The script was not saved because the input argument replaceExisting = \\\"
no\\\", and the following error occurred:\" & return & return & tab & \"(\" & n & \") \" & m
                       else
                           set m to \"The script was not saved for the following reason:\" & return & return & tab & \"(\" & n & \") \" & m
                       end if
                       error m
                   end if
               end try
           end considering
       end storeScriptTemp"

   set tempPath to do shell script "" & ¬
       "f=\"/tmp/$(uuidgen).scpt\"" & linefeed & ¬
       "osacompile -o $f <<<" & tempScript's quoted form & linefeed & ¬
       "{ sleep 60 ; rm -f $f ; } &>/dev/null &" & linefeed & ¬
       "echo $f"
   return (load script tempPath)'s storeScriptTemp(compiledScript, destinationFilePath, replaceExisting)
end storeScript

Here is a demonstration of the inline usage of this handler:

Applescript:


use framework "Foundation"
use scripting additions

-- This is the sample compiled script object created within code to be saved to disk
script FooBar
   property foo : "bar"
end script

-- This is the destination file where the script object is to be saved
set destinationFilePath to (path to desktop)'s POSIX path & "MyFooBarScript.scpt"

-- The following command would fail to save the compiled script object to disk because the <store script> command is in the scope of a <use framework "Foundation"> command
-- store script FooBar in destinationFilePath replacing yes

-- The following handler call succeeds in saving the compiled script object to disk because the handler's <store script> command is outside the scope of a <use framework "Foundation"> command
my storeScript({compiledScript:FooBar, destinationFilePath:destinationFilePath, replaceExisting:"yes"})

-- This shows that the saved script object can be successfully retrieved and used
set savedScriptObject to (load script destinationFilePath)
display dialog savedScriptObject's foo --> displays "bar" in a dialog window

on storeScript({compiledScript:compiledScript, destinationFilePath:destinationFilePath, replaceExisting:replaceExisting})
   set tempScript to "
       on storeScriptTemp(compiledScript, destinationFilePath, replaceExisting)
           considering diacriticals, hyphens, punctuation and white space but ignoring case and numeric strings
               try
                   set {scriptSaved, savedFilePath} to {false, missing value}
                   if destinationFilePath = missing value then
                       store script compiledScript -- the user will be prompted for a destination file
                   else
                       if replaceExisting = \"yes\" then
                           store script compiledScript in destinationFilePath replacing yes
                           set savedFilePath to destinationFilePath
                       else if replaceExisting = \"no\" then
                           store script compiledScript in destinationFilePath replacing no
                           set savedFilePath to destinationFilePath
                       else if replaceExisting = \"ask\" then
                           try
                               set preexistingFileFound to false
                               destinationFilePath as POSIX file as alias
                               set preexistingFileFound to true
                           end try
                           store script compiledScript in destinationFilePath replacing ask
                           if not preexistingFileFound then set savedFilePath to destinationFilePath
                       end if
                   end if
                   set scriptSaved to true
                   return {scriptSaved:scriptSaved, savedFilePath:savedFilePath}
               on error m number n
                   if n ≠ -128 then
                       if replaceExisting = \"no\" then
                           set m to \"The script was not saved because the input argument replaceExisting = \\\"
no\\\", and the following error occurred:\" & return & return & tab & \"(\" & n & \") \" & m
                       else
                           set m to \"The script was not saved for the following reason:\" & return & return & tab & \"(\" & n & \") \" & m
                       end if
                       error m
                   end if
               end try
           end considering
       end storeScriptTemp"

   set tempPath to do shell script "" & ¬
       "f=\"/tmp/$(uuidgen).scpt\"" & linefeed & ¬
       "osacompile -o $f <<<" & tempScript's quoted form & linefeed & ¬
       "{ sleep 60 ; rm -f $f ; } &>/dev/null &" & linefeed & ¬
       "echo $f"
   return (load script tempPath)'s storeScriptTemp(compiledScript, destinationFilePath, replaceExisting)
end storeScript

Edit note: Minor edits were made to the script and comments from the original posting.

Last edited by bmose (2019-12-05 09:12:01 am)

Offline

 

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)