You are not logged in.
Pages:: 1
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
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
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
Online
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.
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
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
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
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?
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
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
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
Online
… 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).
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
Online
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
Sorry for the delay in responding.
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.
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
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
(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
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
I'm resurrecting this old thread because I've found a better solution than the one presented in the last two posts above.
Just to recap, the problem is that AppleScript's store script command doesn't work when it is in the scope of an ASObjC use framework "Foundation" statement. The previous two posts demonstrated a workaround that involves placing the store script command in an external script file on disk lacking a use framework "Foundation" statement and passing the compiled script object to be saved to that external script. That technique, while effective, is cumbersome to implement.
While working on some metaprogramming projects, I found a far better solution. It involves placing the store script command in a script object lacking a use framework "Foundation" statement that is created metaprogrammatically via a run script command within the original script (that does have a use framework "Foundation" statement), and passing the compiled script object to be saved to that metaprogrammatically created script object:
Applescript:
set metascriptText to "
script
on storeScript(scr, pth, rpl)
store script scr in pth replacing rpl
end storeScript
end script
"
set metascriptObj to (run script metascriptText)
metascriptObj's storeScript(scriptToBeSaved, destinationPath, replaceDirective)
(*
where:
scriptToBeSaved = compiled script object to be saved to disk
destinationPath = POSIX path to disk location where the script object is to be saved; the type of the saved script is determined by the POSIX path's file extension:
.scpt [or no extension] = regular script file
.scptd = script bundle
.app = applet
.applescript = text file
replaceDirective = one of the following (unquoted!!) constants
yes = replace an existing file of the same name
no = quit without replacing an existing file of the same name
ask = prompt the user in the case of an existing file of the same name
*)
Here is sample code demonstrating its use:
Applescript:
use framework "Foundation"
use scripting additions
script SampleCompiledScriptObjectToBeSaved
tell application "Finder"
activate
display dialog "I was successfully saved!"
end tell
end script
set scriptToBeSaved to SampleCompiledScriptObjectToBeSaved
set destinationPath to ((path to desktop as text) & "SampleScript.scpt")'s POSIX path
set replaceDirective to yes
-- The following command fails because the <store script> command is within the scope of the top-level <use framework "Foundation"> statement
--store script scriptToBeSaved in destinationPath replacing replaceDirective
-- The following code succeeds because the <store script> command in the metaprogrammatically created anonymous script object is not within the scope of the top-level <use framework "Foundation"> statement
set metascriptText to "
script
on storeScript(scr, pth, rpl)
store script scr in pth replacing rpl
end storeScript
end script
"
set metascriptObj to (run script metascriptText)
metascriptObj's storeScript(scriptToBeSaved, destinationPath, replaceDirective)
-- This demonstrates that the script was successfully saved
run script destinationPath --> displays "I was successfully saved!"
The code can be wrapped in a handler for easy re-use:
Applescript:
on storeScript:scriptToBeSaved inPath:destinationPath replacing:replaceDirective
set metascriptText to "
script
on storeScript(scr, pth, rpl)
store script scr in pth replacing rpl
end storeScript
end script
"
set metascriptObj to (run script metascriptText)
metascriptObj's storeScript(scriptToBeSaved, destinationPath, replaceDirective)
end storeScript:inPath:replacing:
-- which would then be called with a handler call such as the following:
my storeScript:scriptToBeSaved inPath:destinationPath replacing:replaceDirective
It can even be condensed into a single line of code, if desired:
Applescript:
(run script "script" & return & "on storeScript(scr, pth, rpl)" & return & "store script scr in pth replacing rpl" & return & "end storeScript" & return & "end script")'s storeScript(scriptToBeSaved, destinationPath, replaceDirective)
Incidentally, parameters can be passed to the metaprogrammatically created script object as script properties, and the store script command executed by a run command:
Applescript:
set metascriptText to "
script
property scr:missing value
property pth:missing value
property rpl:missing value
store script (my scr) in (my pth) replacing (my rpl)
end script
"
set metascriptObj to (run script metascriptText)
set {metascriptObj's scr, metascriptObj's pth, metascriptObj's rpl} to {scriptToBeSaved, destinationPath, replaceDirective}
run metascriptObj
However, I prefer the first technique, i.e., passing parameters as handler arguments and executing the store script command by a handler call, simply because it involves less coding.
Tested in 10.13 High Sierra, 10.15 Catalina, and 12 Monterey.
Last edited by bmose (2021-12-02 09:16:50 pm)
Offline
Pages:: 1