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:


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:


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

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.

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.

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

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:

-- 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.

My bad. I didn’t mean to include a “.scpt” extension.

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:

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.”

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:

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?

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.

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?

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.

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

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

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

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:

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

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

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
1 Like

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.

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.

Sorry for the delay in responding.

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.

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.

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):


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):


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:


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.

1 Like

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

1 Like

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”.


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:


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.

1 Like

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:

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:


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:


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:


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


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.

1 Like