(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:
-- 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:
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:
(*
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.