Constructing a variable from text

It is driving me nuts that I can’t recall how to do this:

set myRec to {alpha:22, beta:43, delta:66}
set A to characters 1 thru 5 of "alphabet" as string

-- set Num to myRec's A cast as a variable name.

Is there a way that I’ve simply forgotten and can’t find?

There’s no proper way to do, but a workaround is to use run script.

Not in any form I can imagine - I’ve tried a lot of variations using run script.

I came up with something that works, though it is a bit ugly:

set myRec to {alpha:22, beta:43, delta:66}
set A to characters 1 thru 5 of "alphabet" as string

to makePropGetter(propName, appName)
	local usingPrologue, usingEpilogue
	set usingPrologue to "using terms from application \"" & appName & "\""
	set usingEpilogue to "end using terms from"
	if appName is equal to "" then
		set usingPrologue to ""
		set usingEpilogue to ""
	end if
	run script "
script
	to getPropFrom(rec)
" & usingPrologue & "
		return " & propName & " of rec
" & usingEpilogue & "
	end getPropFrom
end script
"
end makePropGetter

-- set Num to myRec's A cast as a variable name.
set aGetter to makePropGetter(A, "")
tell aGetter to set Num to getPropFrom(myRec)
log result

-- Examples with application specific property names:

using terms from application "Finder"
	set myFinderRec to {alias file:38, application file:89, document file:64, item:99}
end using terms from
set aliasFileGetter to makePropGetter("alias file", "Finder")
tell aliasFileGetter to getPropFrom(myFinderRec)
log result
tell makePropGetter("application file", "Finder") to getPropFrom(myFinderRec)
log result
tell makePropGetter("document file", "Finder") to getPropFrom(myFinderRec)
log result
-- no need for application terms since item is a generic term (class?)
tell makePropGetter("item", "") to getPropFrom(myFinderRec)
log result

Of course, this only works for records, not actual variables. Also, it looks like the List & Record Tools OSAX can do this same stuff, probably more efficiently.

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Hi Adam.

Try this:

on performStringCommand(command, onObject)
	run script "on run {anObject}" & return & command & " of anObject" & return & "end run" with parameters {onObject}
end performStringCommand


set myRec to {alpha:22, beta:43, delta:66}

performStringCommand("alpha of myRec", me)

performStringCommand("myRec's beta", me)

performStringCommand("delta", myRec)

performStringCommand("delta", performStringCommand("myRec", me))

It’s the same way chrys is doing it, using run script as Bruce suggested, except in a ‘prettier’ form.

run script operates in a separate script or environment, so the local one (me) needs to be passed as a parameter.

Wow. :cool:

I would no more have come up with that methodology than fly by flapping my arms; but it works. I confess to not fully understanding the variations, however. The third one does it all, the others elude me.

Adam

Afterthought: Amazingly flexible:

to getFromRecord(VblName, RecName)
	run script "on run {tScript}" & return & VblName & " of tScript" & return & "end run" with parameters {RecName}
end getFromRecord

set myRec to {alpha:"Adam", beta:{"A", "B", "C"}, delta:66, zeta:"Hello World", epsilon:{A:"blue", B:"red"}}
set Vars to {"alpha", "beta", "delta", "zeta", "epsilon"}
set Ans to {}

repeat with aVar in Vars
	set end of Ans to getFromRecord(aVar, myRec)
end repeat

set myColor to A of (getFromRecord("epsilon", myRec))

That’s really interesting how you did that guys. I’m trying to understand it and I do, but now I’m trying to apply it to something else. Since strings can be used as variables, as you’ve shown, then can strings be used to create variables like the following?

If I have 2 lists in a script, can I combine them to make a record? for example…

take these two lists
set variableList to {“a1”, “a2”}
set stringList to {“cat”, “dog”}

and get this record by combining them with a repeat loop or something
{a1:“cat”, a2:“dog”}

regulus6633,

Here is something that seems to do what you asked:

on run
	set variableList to {"a1", "a2"}
	set stringList to {"cat", "dog"}
	
	set newRecord to makeRecordWithStringProperties(variableList, stringList)
	
	-- Do some tests, the log should show all true results
	log a1 of newRecord is equal to "cat"
	log a2 of newRecord is equal to "dog"
end run

to makeRecordWithStringProperties(props, values)
	if (count of props) is not equal to (count of values) then ¬
		error "List of properties and values must be the same length."
	local theScript
	set theScript to "
on run valueList
{ " & item 1 of props & ": item 1 of valueList"
	repeat with i from 2 to count of props
		set theScript to theScript & ¬
			", " & item i of props & ": item " & i & " of valueList"
	end repeat
	set theScript to theScript & " }
end
"
	run script theScript with parameters values
end makeRecordWithStringProperties

It does not correctly handle application-specific property names (like I tried to enable in my earlier script in this thread with the conditional using terms from app … stuff), but I think it will work for user-created property names (shows up as green in Script Editor) as well as system-wide property names (from AppleScript, or an installed OSAX; compilable outside of a using terms from app … or tell app … block resulting in blue text in Script Editor).

It does chrys, thanks! Unfortunately although I can understand it somewhat, I can’t seem to reproduce any of my own code using those techniques. I can’t figure out what needs to be in quotes and what doesn’t. I can’t figure out why some of the variables from the handler get inserted as “parameters” and others do not… and some other things too.

I’ve searched here and google for tutorials on the “run script” command but haven’t turned up anything useful. Can anyone direct me to some? I couldn’t find anything useful in the applescript language guide either! I’d certainly like to get a better feel for how to use these. Of course when recommending any tutorials, remember that I’ve only been scripting for a short time, but any help would be appreciated.

I can’t direct you to a reference, R63, but perhaps I can explain a bit feebly:

set vbls to {"firstName", "lastName", "age"}
set vals to {"Adam", "Bell", 70}

set recText to "{" & item 1 of vbls & ":" & "\"" & item 1 of vals & "\"," & item 2 of vbls & ":" & "\"" & item 2 of vals & "\"," & item 3 of vbls & ":" & item 3 of vals & "}"
set Rec to (run script recText)
--> {firstname:"Adam", lastname:"Bell", age:70}

Here’s a simple repeat to create a record given two lists of three items each (no test that they match). Notice that without the escaped quotes it looks like this:

“{” & item 1 of vbls & “:” & item 1 of vals & “,” & item 2 of vbls & “:” & item 2 of vals & “,” & item 3 of vbls & “:” & item 3 of vals & “}”

which simply constructs what the record would look like in plain text. When we run that (with the escaped quotes where we need them in the variables) we get a record.

What you are doing is constructing something that looks like an applescript statement and creating it by running it.

A simpler example:

set scpt to "set B to 12" & return & "set C to 13" & return & "set D to B * C"
set ans to run script scpt --> 156

but the variables B, C, and D are part of the external script, not variables in the one that ran it.

Note: Many programming languages have a feature that allows a program to evaluate a string as code at run-time. Almost invariably, the common advice is to only use such a feature as a last resort. If there is an alternative to evaluating a string as code, it is usually faster and considered better style to go with the alternative. In this case, List & Record Tools OSAX has some functionality that can do both the property value extraction and the record creation functions discussed in this thread. Usually, I would expect OSAX-based methods to be faster, but this is not the case here (see my timings at the bottom of this post). In some cases though, there may still be better ways to structure the surrounding application so that it works without having to do this dynamic record access in the first place.


Maybe it would help if wrote up an expanded version of how I came up with the code. For code generation tasks like this, it is often easier to work backwards. Since the goal is to generate a string that contains some source code, start with the code that you actually want to generate. Since some part of the generated code will vary based on the input data, it is best to have multiple examples at hand so you can analyze them for common elements. Here are some examples similar to your original scenario that cover lengths 0 through 3:

-- Starting Code String Examples
"{}"
"{a1:\"furry\"}"
"{a1:\"feline\", b2:\"canine\"}"
"{a1:\"cat\", b2:\"dog\", c3:42}"

Note: These strings are in the same format that you would need to type them in as a string so that run script will work properly. That is why the inner double quotes need backslashes. This format is also the same as shown in the Result “tab” of Script Editor.

STEP 1

Using multiple examples helps to find out what parts change based on the size of the input(s). From these examples, we can see that the only things that do not change are the open brace and the close brace. Everything else depends on how many things we want to put inside. So there is some simple code we can start with:

local insideStuff, theScript
-- Uncomment one of these for testing purposes:
--set insideStuff to ""
--set insideStuff to "a1:\"furry\""
--set insideStuff to "a1:\"feline\", b2:\"canine\""
--set insideStuff to "a1:\"cat\", b2:\"dog\", c3:42"

set theScript to "{" & insideStuff & "}"

Since all the code is doing is adding open and close braces, the only difference between the input strings and the output strings is the braces. So now we need to find another bit of the strings that we can generate. What goes inside the braces? A (possible empty) sequence of pairs belongs there. To my thinking, there are two separate concepts at work in that description: the sequence and the pairs. In this case, I like to tackle the outer-most structure first: the sequence.

STEP 2

The next step is to write some code that can generate a comma-delimited string from a list of other strings. To me, this seems like an ideal time to create a handler:

local pairStrings, insideStuff, theScript
-- Uncomment one of these for testing purposes:
--set pairStrings to {}
--set pairStrings to {"a1:\"furry\""}
--set pairStrings to {"a1:\"feline\"", "b2:\"canine\""}
--set pairStrings to {"a1:\"cat\"", "b2:\"dog\"", "c3:42"}

set insideStuff to insertCommas(pairStrings)
set theScript to "{" & insideStuff & "}"

to insertCommas(aList)
	if (count of aList) is equal to 0 then return ""
	local theResult, i
	set theResult to item 1 of aList as text
	repeat with i from 2 to count of aList
		set theResult to theResult & ", " & item i of aList
	end repeat
	return theResult
end insertCommas

Incidentally, one of the reasons this might make a good handler is because there are other ways of accomplishing the same goal. Here is a different version that uses AppleScript’s text item feature to insert the commas in the right places:

to insertCommas(aList)
	local otid, theResult
	set otid to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to ", "
		set theResult to aList as text
		set AppleScript's text item delimiters to otid
	on error msg number n
		set AppleScript's text item delimiters to otid
		error msg number n
	end try
	return theResult
end insertCommas

STEP 3

Anyway, the next step in breaking this thing down is to generate the strings for the individual pairs. So here I break the input into a couple of lists and write some code to reassemble the lists into the code strings we want.

local pairStrings, insideStuff, theScript
-- Uncomment one of these pairs of sets for testing purposes:
--set propNames to {}
--set propValueStrings to {}

--set propNames to {"a1"}
--set propValueStrings to {"\"furry\""}

--set propNames to {"a1", "b2"}
--set propValueStrings to {"\"feline\"", "\"canine\""}

--set propNames to {"a1", "b2", "c3"}
--set propValueStrings to {"\"cat\"", "\"dog\"", "42"}

set pairStrings to generatePairStrings(propNames, propValueStrings)
set insideStuff to insertCommas(pairStrings)
set theScript to "{" & insideStuff & "}"

to generateValueString(i, valueList)
	item i of valueList
end generateValueString

to generatePairStrings(propNames, propValueStrings)
	local theResult, i
	set theResult to {}
	repeat with i from 1 to count of propNames
		set theResult to ¬
			theResult & ¬
			(item i of propNames & ¬
				":" & ¬
				generateValueString(i, propValueStrings))
	end repeat
	return theResult
end generatePairStrings

-- insertCommas not shown here

STEP 4

At this point, the propNames list that the script is using is functionally identical to your original variableList, so that part is accomplished. The next step is to deal with the values. The problem here is coming up with a way to generate a code string for the all the different values you might want to use. Strings are fairly straightforward to handle: escape any existing double quotes or backslashes, then wrap the results in double quotes. Numbers are easy: just put in the string representation, no extra quotes or anything else is needed. But what about other AppleScript types? Probably you could come up with string representations of most values, but I do not know of a way to handle them without doing something ugly like if class of val is string … else if class of val is integer or class of val is real … else if … end. If someone wanted to use the code with a value from some class that is not already handled, they would first have to extend the code to handle the new class. Besides, there are things like references and some raw data values that might not be expressible directly as code strings.

Maybe for your purpose you actually only want to use strings for the values, but to be a bit more useful, it would be nice to be able to handle any kind of value, not just strings. To accomplish this, we will need to pass the values list directly to the code, so that we do not have to transcribe the values into a string representation suitable for use in AppleScript code. But now we have a problem: the code we have been working with does not take any parameters, it is just a single statement that creates a record.

In AppleScript, putting statements in the top-level of a script is the same as putting them in an explicit run handler. This means that the following scripts are equivalent:

set a to doSomething()
set b to doSomethingElse(a)
set c to doAnotherThing(b, "setting c")
on run
	set a to doSomething()
	set b to doSomethingElse(a)
	set c to doAnotherThing(b, "setting c")
end run

Now, normally the run handler does not take parameters, but there is a way to do so (and it works nicely with run script):

on run {x,y,z}
-- Code that uses x, y, and/or z
end run

This will not work for normal scripts because they do not take arguments through the run handler. As far as I know, this is only used when invoking scripts from the command line with osascript, when doing run script foo with parameters, and when invoked as an action by Automator. So this may not even compile on Mac OS X older than 10.4.

Anyway, this method gives us a way to pass actual AppleScript objects to the code we are generating. So instead of trying to transcribe the values into a code string, we can just pull the values out of a list passed directly to the generated code. The generic code looks something like this:

local valueList
set valueList to {"A", "b", 3, path to startup disk}
set theScript to "on run {myValueList}
	-- Use values from myValueList in code here
end"
run script theScript with parameters {valueList}

To give our code a parameter we need to put the code we have been generating in place of the comment in the code string above. This means we need to wrap some more text around the braces. Then we have to generate code to use values from a list, instead of generating code based on the values in a list. This is the key difference between what we do with the property names and the property values. This may be the area where things are getting muddled when you apply this technique in your own code.

local propNames, propValues, pairStrings, insideStuff, recordCode, theScript
-- Uncomment one of these pairs of sets for testing purposes:
--set propNames to {}
--set propValues to {}

--set propNames to {"a1"}
--set propValues to {"furry"}

--set propNames to {"a1", "b2"}
--set propValues to {"feline", "canine"}

--set propNames to {"a1", "b2", "c3"}
--set propValues to {"cat", "dog", 42}

set pairStrings to generatePropertyStrings(propNames)
set insideStuff to insertCommas(pairStrings)
set recordCode to "{" & insideStuff & "}"
set theScript to "on run {myValueList}" & return & recordCode & return & "end run"
run script theScript with parameters {propValues}

to generateValueAccessString(num)
	"item " & num & " of myValueList"
end generateValueAccessString

to generatePropertyStrings(propNames)
	local theResult, i
	set theResult to {}
	repeat with i from 1 to count of propNames
		set theResult to ¬
			theResult & ¬
			(item i of propNames & ¬
				": " & generateValueAccessString(i))
	end repeat
	return theResult
end generatePropertyStrings

-- insertCommas not shown here

There are four changes here from the previous version: 1) the propValues list is now functionally identical to your original stringList, 2) the code in the generated script is now in a run handler that takes a parameter, 3) the script is now being run and passed parameters, 4) the script text now has code to pick out values from a list variable instead of putting the values directly in the generated code.

-- Generated Code Strings After Step 4
"on run {myValueList}
	{}
end run"
"on run {myValueList}
	{a1: item 1 of myValueList}
end run"
"on run {myValueList}
	{a1: item 1 of myValueList, b2: item 2 of myValueList}
end run"
"on run {myValueList}
	{a1: item 1 of myValueList, b2: item 2 of myValueList, c3: item 3 of myValueList}
end run"

As this point the code is functionally very close to what I originally wrote. The main difference is that originally I did it all in one loop instead of doing the property pair string generation and comma insertion in different steps. My original code also had a bug in handling input lists with zero items.

STEP 5

Now, it would be nice to pull the code we have into a handler so that it is reusable without having to copy and paste the calls to generatePropertyStrings(), insertCommas(), and run script. Here is the code again with all the handlers in place and a fancy on run handler that demonstrates the usage of the main handler and runs tests that generate the examples I have been using in this post. I also added an if statement to check that the sizes of the property names list and the property values list are the same.

on run
	local example
	repeat with example in {¬
		{{}, {}, ¬
			{}}, ¬
		{{"a1"}, {"furry"}, ¬
			{a1:"furry"}}, ¬
		{{"a1", "b2"}, {"feline", "canine"}, ¬
			{a1:"feline", b2:"canine"}}, ¬
		{{"a1", "b2", "c3"}, {"cat", "dog", 42}, ¬
			{a1:"cat", b2:"dog", c3:42}} ¬
			}
		local propNames, propValues, expectedResult, theResult
		set {propNames, propValues, expectedResult} to example
		set theResult to makeRecordWithStringProperties(propNames, propValues)
		if theResult is not equal to expectedResult then
			return {"Bad result: ", theResult, " expected: ", expectedResult}
		end if
	end repeat
end run

to makeRecordWithStringProperties(propNames, propValues)
	if (count of propNames) is not equal to (count of propValues) then ¬
		error "Size of property names list and property values list must be the same."
	local pairStrings, insideStuff, recordCode, theScript
	set pairStrings to generatePropertyStrings(propNames)
	set insideStuff to insertCommas(pairStrings)
	set recordCode to "{" & insideStuff & "}"
	set theScript to "on run {myValueList}" & return & recordCode & return & "end run"
	run script theScript with parameters {propValues}
end makeRecordWithStringProperties

to generateValueAccessString(num)
	"item " & num & " of myValueList"
end generateValueAccessString

to generatePropertyStrings(propNames)
	local theResult, i
	set theResult to {}
	repeat with i from 1 to count of propNames
		set theResult to ¬
			theResult & ¬
			(item i of propNames & ¬
				": " & generateValueAccessString(i))
	end repeat
	return theResult
end generatePropertyStrings

to insertCommas(aList)
	if (count of aList) is equal to 0 then return ""
	local theResult, i
	set theResult to item 1 of aList as text
	repeat with i from 2 to count of aList
		set theResult to theResult & ", " & item i of aList
	end repeat
	return theResult
end insertCommas

And now we have a nicely reusable version of what was developed piecewise in this post. Hopefully the explanations are clear and useful.


Now that all of that is out of the way, I will point out that if you install the List & Record Tools OSAX that I mentioned at the top of this post, the following examples accomplish the same thing:

Using the handlers developed in this post:

set p to {"a1", "b2", "c3", "d4"}
set v to {"cat", "dog", 42, path to startup disk}
makeRecordWithStringProperties(p, v)

Using the List & Record Tools OSAX:

set p to {"a1", "b2", "c3", "d4"}
set v to {"cat", "dog", 42, path to startup disk}
set user property p in {} to v

With those property names and values, I ran both version 1000 times. The makeRecordWithStringProperties version took 105 seconds, and the set user property version took 173 seconds. So, to my surprise, the pure AppleScript version is faster.

Model: iBook G4 933
AppleScript: 1.10.7
Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Thanks so much Adam and Chris. I appreciate you taking the time to explain some of this. It’s going to take me awhile to get through all of it, but I wanted to say thanks in the mean time. After I finish studying it I’ll let you know what I think.