How to get names of record properties?

Hi,

I understand that AppleScript doesn’t provide a direct means of getting the names of a record’s properties. I put together a handler that gets the job done, but it’s long and somewhat sluggish. I suspect the topic might have come up before, but I couldn’t find anything in the forums. Any suggestions on a more elegant solution to this problem would be much appreciated.

bmose


get_record_property_names({a:1, bb:"b", ccc:true, dddd:{1, 2, {1, 2, {1, 2}}}, eeeee:{f:{1, 2}, g:{h:1, i:2}}}) --> {"a","bb","ccc","dddd","eeeee"}

on get_record_property_names(the_record)
	
	-- Initialize the result variable to an empty list
	
	set names_of_record_properties to {}
	
	-- Exit with an error if input parameter is not a record
	
	if class of the_record is not record then error "Not a record"
	
	try
		
		-- Force the error message "Can't make {...record property names and values...} into type null."
		
		the_record as null
	on error error_message
		
		-- Capture "...record property names and values..." from the error message
		
		set current_string to text 13 thru -18 of error_message
	end try
	
	-- Parse the "...record property names and values..." string and extract its property names 
	
	repeat until current_string is ""
		
		-- Capture the next property name (text up to but not including the first ":")
		
		set name_of_current_record_property to text 1 thru ((offset of ":" in current_string) - 1) of current_string
		
		-- Deblank the captured property name
		
		repeat while name_of_current_record_property starts with space
			set name_of_current_record_property to text 2 thru -1 of name_of_current_record_property
		end repeat
		repeat while name_of_current_record_property ends with space
			set name_of_current_record_property to text 1 thru -2 of name_of_current_record_property
		end repeat
		
		-- Add the property name to the result list
		
		set end of names_of_record_properties to name_of_current_record_property
		
		try
			
			-- Break the "...record property names and values..." string into fragments at each comma in the string (one of which will represent the end of the current property name:property value combination)
			
			set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, ","}
			set current_string_fragments to text items of current_string
			
			set current_record_item_found to false
			repeat with i from 1 to length of current_string_fragments
				try
					
					-- Use a "run script" macro to reconstruct progressively larger fragments of "...record property names and values..." from left to right until a valid property name:property value combination corresponding to the just saved property name is created
					
					set s to "on run {the_record}"
					set s to s & return & "if {" & ((items 1 thru i of current_string_fragments) as string) & "}'s " & name_of_current_record_property & " is the_record's " & name_of_current_record_property & " then return true"
					set s to s & return & "return false"
					set s to s & return & "end run"
					set current_record_item_found to run script s with parameters {the_record}
				end try
				
				-- As soon as the appropriate property name:property value combination is created, exit the repeat loop
				
				if current_record_item_found then exit repeat
			end repeat
			if current_record_item_found then
				if i < (length of current_string_fragments) then
					
					-- If there are more property name:property value entries left in the record (other than the current entry), reset the "...record property names and values..." string to just those remaining entries 
					
					set current_string to (items (i + 1) thru -1 of current_string_fragments) as string
				else
					
					-- Otherwise, quit processing the string
					
					set current_string to ""
				end if
				set AppleScript's text item delimiters to astid
			else
				error
			end if
		on error
			set AppleScript's text item delimiters to astid
			
			-- Capture any errors occurring during extraction of property names
			
			error "Problem parsing record names.  The following residual string was not processed:" & return & return & current_string
		end try
	end repeat
	
	-- Return the record's property names as a list of strings
	
	return names_of_record_properties
	
end get_record_property_names

Hi,

I know only this dirty method to “coerce” a record to a string

coerce_record_to_string({a:1, b:2, c:3}) --> {a:1, b:2, c:3}

on coerce_record_to_string(theRecord)
	try
		return theRecord as string
	on error errorMessage
		set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "Can't make "}
		set theRecord to item 2 of text items of errorMessage
		set AppleScript's text item delimiters to " into type string."
		set theRecord to item 1 of text items of theRecord
		set AppleScript's text item delimiters to ASTID
		return theRecord
	end try
end coerce_record_to_string

Stefan,

Thank you for your reply. The string version of the record that your technique generates still of course needs to be parsed to get the record property names. It was the parsing step that caused the proliferation of lines of code that I encountered and that I was seeking to reduce. One approach might be a clever regular expression that extracts just the property names.

bmose

See:

http://www.latenightsw.com/freeware/RecordTools/index.html

http://applemods.sourceforge.net/mods/Data/Record.php

That said, why do you need to extract record labels in the first place?

has

hhas,

Thanks for the pointer to List & Record Tools. It’s unfortunate that one has to resort to a scripting addition or a “dirty” script to do something that AppleScript should implement.

I’ve had a few occasions where retrieval of record labels would be helpful. The current need came up as I was writing a handler that displays values of objects of any arbitrary class, including records, in a dialog box. In the case of records, I wanted the handler to display each of the record’s items on a separate line, ie one “label:value” item per line. Since the handler does not know either the class or the value of its input parameter, I wrote my “dirty” script to handle the case of it being a record.

bmose

TextCommands’ ‘format’ command has a pretty printing option that you may find useful here, e.g.:

tell application "TextCommands"
	format {1, true, "Hello!", {x:1, name:"foo"}} with pretty printing
end tell

-- Result:
"{
	1,
	true,
	\"Hello!\",
	{
		name:\"foo\",
		x:1
	},
}"

Main issue with using TextCommands or List & Record Tools that you can’t pass application references to them (that’s an AppleScript limitation, not an app/osax one), which may or may not be a problem for you. If it is, you’ll have to go with the vanilla hack approach, although that may introduce problems of its own, of course.

HTH

has

has,

Thanks for pointing out the TextCommands osax. Along with many other things, it does exactly what my “display value” handler was designed to accomplish. It looks like you are the author → nice work, lots of functionality!!

bmose

Oops…should have said TextCommands application (not osax).

Thanks for this thread - still useful more than 5 years later. Below are subroutines that utilize do shell script and regular expressions to achieve coercion to string and extracting property names. Please note the caveats in the comments, esp. for the property-name-extraction routine.


		(* 
			Returns a string representation of the specified record.
			
			CAVEAT:	As of AppleScript 2.2.3 (OS X 10.8.2), coercing a record to a string is NOT directly supported.
					This subroutines relies on a hack that exploits the fact that, when such a coercion is attempted,
					the error message itself (!) contains the desired string representation of the record.
					This subroutine will continue to work as long as this holds true OR once a future
					AppleScript version does support direct coercion of a record to a string.
		*)
		on getRecordAsText(theRecord)
			if class of theRecord ≠ record then error "Expected input of class record, but found " & class of theRecord & " instead." number -1700
			local theRecordAsText
			try
				set theRecordAsText to theRecord as string -- This will FAIL as of AS 2.2.3 (OS X 10.8.2), but who knows what the future brings.
			on error errorMessage
				-- Trick: We exploit the fact that the error message contains the desired string representation of the record, so we extract it from there.
				set theRecordAsText to do shell script "egrep -o '\\{.*\\}' <<< " & quoted form of errorMessage
			end try
			return theRecordAsText
		end getRecordAsText

		(* 
			Returns a list of all property names of the specified record. 
				
			CAVEATS:
					The list returned is always FLAT:
					Even if the record is nested (contains sub-records); in other words: while the list
					returned always contains all property names, it will not reflect the hierarchical structure of nested input records.
		
					The list may contain FALSE POSITIVES:
					This subroutine uses simple regular expressions to parse the string representation of the given record.
					Since this approach does not properly parse the structure of a  record, false positives inside string-property
					*values* can result.
					Specifically, words that are preceded by either "{" or ", " and immediately followed by ":" inside string-property
					values yield false matches; therefore, for instance, do not pass in records that have string properties that contain
					string representations of records themselves.
					In theory, you could write a recursive subroutine to walk the list of names returned - appropriately
					recursing when a nested property is found - to weed out the false positives.
		*)
		on getPropertyNames(theRecord)
			if class of theRecord ≠ record then error "Expected input of class record, but found " & class of theRecord & " instead." number -1700
			local theRecordAsText
			-- Get a string representation of the record.
			try
				set theRecordAsText to theRecord as string -- This will FAIL as of AS 2.2.3 (OS X 10.8.2), but who knows what the future brings.
			on error errorMessage
				-- Trick: We exploit the fact that the error message contains the desired string representation of the record, so we extract it from there. This still works as of AS 2.2.3 (OS X 10.8.2)
				set theRecordAsText to do shell script "egrep -o '\\{.*\\}' <<< " & quoted form of errorMessage
			end try
			-- Extract the property names with `egrep` using regular expressions; note that the |.| matching is for property names that would otherwise not be valid identifiers.
			-- !! See note about potential false positives above.
			return paragraphs of (do shell script "egrep -o $'({|, )(\\w+|\\|[^|]+\\|):' <<< " & quoted form of theRecordAsText & " | egrep -o '(\\w+|\\|[^|]+\\|)'")
		end getPropertyNames


One more time, ASObjC Runner by Shane Stanley may be useful.


tell application "ASObjC Runner"
 all labels of {theFirst:1, theSecond:2, theThird:3}
 --> {"theFirst", "theSecond", "theThird"}
 all labels of {theFirst:1, theSecond:2, theThird:1} for values {1}
 --> {"theFirst", "theThird"}
end tell

Yvan KOENIG (VALLAURIS, France) vendredi 5 octobre 2012 17:58:50

Thanks, Yvan, that’s good to know. ASObjC Runner seems to contain tons of useful features and can be found at http://www.macosxautomation.com/applescript/apps/runner.html.

Relying on a third-party application is not always an option, though, and my do shell script-based code is a reasonably robust solution in that event.

This returns an indented list but isn’t robust with regard to barred labels:

on record_labels_to_text(theRecord)
	set tmpFile to POSIX path of ((path to temporary items as text) & "rcrd.dat")
	
	set fRef to (open for access tmpFile with write permission)
	try
		set eof fRef to 0
		write theRecord to fRef
	end try
	close access fRef
	
	run script (do shell script ("osascript -e 'read \"" & tmpFile & "\" as record' -s s |
	sed -E 's/\\\\\\\"//g ;			# Zap all double-quote characters in text values to simplify the next substitution.
	s/:\\\"[^\\\"]*\\\"([,}])/:\\1/g ;	# Zap all text values in the record text.
	s/([{}])[^:{}]+([{}])/\\1\\2/g ;	# Zap all non-records from lists.
	:loop
		s/{}//g ;				# Zap all now-empty lists and lists containing only them.
		t loop
	s/:[^,{}]*([,}])/:\\1/g ;			# Zap all remaining values.
	s/:/\\\"/g ;					# Replace all colons with quotes.
	s/({|, )([^{])/\\1\\\"\\2/g ;		# Insert quotes after left braces or comma-spaces where not followed by left braces.
	s/\\\"{/\\\", {/g ;				# Insert any missing comma-spaces between quotes and left braces.'"))
end record_labels_to_text

set fred to {a:1, b:2, c:3, |name 1|:"Aardvark \"{a:1, b:2}\" Gathorne", |name 2|:"Sergeant \"Fred\" Squinge", subrecord:{x:{7, 8, 9}, y:{1, 2, {a:{color:"blue"}, b:{{}, 4, {color:"aardvarquene"}, 2, 1}}, 4, 5}, z_:missing value}}
record_labels_to_text(fred)

:cool:

I notice you have started to comment your sed scripts. :wink:

And your script is AWESOME!!!

When it’s for the script’s private use (don’t need to get or set properties by using records in apps) you could consider to use (and tweak) my example code from here. It’s also faster than records.

DJ, thanks for sharing and stretching the bounds of AppleScript

Nigel, this is impressive and edifying stuff. May I suggest tweaks to your code?

  • Use a temporary file that is specific to a given invocation of the subroutine.
  • Delete the temporary file after use.
    See my stab at it below.

From what I understand, your code (a) prevents false positives, (b) correctly reflects the structure of the input record, but (c) can break with funky characters in labels enclosed in |…|, e.g.: “|item {1|”.

I also noticed that barred label names are reported with their enclosing “|” characters - which may or may not be desired.
In the end it seems that our whole no-third-party-tools approach is of limited usefulness in that accessing record properties by label names constructed at runtime does not work, correct?


on record_labels_to_text(theRecord)
	
	local tmpFile, fRef, retVal
	
	set tmpFile to do shell script "mktemp -t 'rltt'"
	
	set fRef to (open for access tmpFile with write permission)
	try
		set eof fRef to 0
		write theRecord to fRef
	end try
	close access fRef
	
	set retVal to run script (do shell script ("osascript -s s -e 'read \"" & quoted form of tmpFile & "\" as record' |
   sed -E 's/\\\\\\\"//g ;            # Zap all double-quote characters in text values to simplify the next substitution.
   s/:\\\"[^\\\"]*\\\"([,}])/:\\1/g ;    # Zap all text values in the record text.
   s/([{}])[^:{}]+([{}])/\\1\\2/g ;    # Zap all non-records from lists.
   :loop
       s/{}//g ;                # Zap all now-empty lists and lists containing only them.
       t loop
   s/:[^,{}]*([,}])/:\\1/g ;            # Zap all remaining values.
   s/:/\\\"/g ;                    # Replace all colons with quotes.
   s/({|, )([^{])/\\1\\\"\\2/g ;        # Insert quotes after left braces or comma-spaces where not followed by left braces.
   s/\\\"{/\\\", {/g ;                # Insert any missing comma-spaces between quotes and left braces.'"))
	
	tell application "System Events" to delete alias tmpFile
	
	return retVal
end record_labels_to_text

Thanks for your comments, guys.

mklement. Thanks for the tip about “mktemp”, which I didn’t know about before. I’ve always felt a little uncomfortable about using the system’s temporary items folder and have only done so in the past because, unlike the user one (which admittedly I haven’t tried recently), it’s purged whenever the machine’s restarted. If the recommendation is that a script should always tidy up after itself anyway ” to which I already subscribe ” then both that and “mktemp” have given me food for thought!

As far as I can tell, yes. Starting off by deleting the text values immediately eliminates anything in them which may be mistaken for part of the record syntax. The problem with barred labels is that they can legally contain similar stuff which you want to keep ” and which could even scupper the removal of the string values. A really foolproof method would need to keep track of where it was and what it was processing in the string representation of the record.

I’ve tackled this label-to-text problem a few ways in the past when the question’s been asked, but only as coding exercises. Practically, if you don’t know a record’s labels (or potential labels) at the time you’re writing a script, there’s no point in using it.

It’s proved beyond my current ability to make this process totally robust in a single shell script, so I’ve used an AppleScript handler to identify and separate out “barred labels” and to identify and remove “text values”. Different shell scripts then process the “barred labels” (if any) and the “other stuff”. Since only a minor modification was needed to let the script handle lists containing records as well, it now does that too.

on record_labels_to_text(recordOrList)
	if (recordOrList's class is record) then
		set inputClass to "record"
	else if (recordOrList's class is list) then
		set inputClass to "list"
	else
		error
	end if
	
	-- Create a temporary file and write the record or list to it.
	set tmpFile to do shell script "mktemp -t 'rltt'"
	set fRef to (open for access tmpFile with write permission)
	try
		set eof fRef to 0
		write recordOrList to fRef
	end try
	close access fRef
	
	-- Get the record or list in text form by reading the file with osascript. Delete the file when read. Segment the text into "barred labels" and "other stuff", the latter having all "text values" removed.
	set textSegments to segment(do shell script ("txt=$(osascript -e 'read \"" & tmpFile & "\" as " & inputClass & "' -s s) ; rm " & quoted form of tmpFile & " ; echo \"$txt\" ;"))
	
	-- The odd-numbered segments are "other stuff", the even-numbered ones "barred labels". Doctor each accordingly.
	repeat with i from 1 to (count textSegments)
		if (i mod 2 is 1) then -- "Other stuff".
			set sedScript to "sed -E 's/([{}])[^:{}]+([{}])/\\1\\2/g ;	# Zap all non-records from lists.
	:loop
		s/{}//g ;				# Zap all now-empty lists and lists containing only them.
		t loop
	s/:[^,{}]*([,}])/:\\1/g ;			# Zap all remaining values.
	s/:/\\\"/g ;					# Replace all colons with quotes.
	s/({|, )([^{]|$)/\\1\\\"\\2/g ;		# Insert quotes after left braces or comma-spaces where not followed by left braces or ends of lines.
	s/\\\"{/\\\", {/g ;				# Insert any missing comma-spaces between quotes and left braces.' <<<"
		else -- "Barred label".
			set sedScript to "sed -E 's/\\\\|\\\"/\\\\&/g ; # Increase any escaping.' <<<"
		end if
		set item i of textSegments to (do shell script (sedScript & quoted form of item i of textSegments))
	end repeat
	
	if (textSegments is {""}) then
		return {} -- The input was a list containing no records.
	else
		set astid to AppleScript's text item delimiters
		set AppleScript's text item delimiters to ""
		set listText to textSegments as text
		set AppleScript's text item delimiters to astid
		
		return (run script listText)
	end if
end record_labels_to_text

-- Given a text representation of a record or list, break it up into segments which are alternately the text delimited by any "barred labels" and the text of the "barred labels" themselves. In the process, lose any "text values" from the delimited segments.
on segment(recordOrListText)
	script o
		property IDs : id of recordOrListText
		property segments : {}
	end script
	
	set barID to id of "|"
	set quoteID to id of quote
	set backslashID to id of "\\"
	set inBarredLabel to false
	set inTextValue to false
	set newSegmentNeeded to true
	set i to 1
	repeat with j from 2 to (count recordOrListText)
		set thisID to item j of o's IDs
		if (thisID is barID) then
			if (inBarredLabel) then
				set end of o's segments to text i thru j of recordOrListText
				set i to j + 1
				set inBarredLabel to false
				set newSegmentNeeded to true
			else if (not inTextValue) then
				if (newSegmentNeeded) then
					set end of o's segments to text i thru (j - 1) of recordOrListText
				else
					set item -1 of o's segments to end of o's segments & text i thru (j - 1) of recordOrListText
				end if
				set i to j
				set inBarredLabel to true
			end if
		else if ((thisID is quoteID) and (item (j - 1) of o's IDs is not backslashID)) then
			if (inTextValue) then
				set i to j + 1
				set inTextValue to false
			else if (not inBarredLabel) then
				if (newSegmentNeeded) then
					set end of o's segments to text i thru (j - 1) of recordOrListText
				else
					set item -1 of o's segments to end of o's segments & text i thru (j - 1) of recordOrListText
				end if
				set i to j
				set inTextValue to true
				set newSegmentNeeded to false
			end if
		end if
	end repeat
	if (newSegmentNeeded) then
		set end of o's segments to text i thru j of recordOrListText
	else
		set item -1 of o's segments to end of o's segments & text i thru j of recordOrListText
	end if
	
	return o's segments
end segment

set fred to {x:"hello", |test lab {a:1, b:2}|:"6", a:1, b:2, c:3, |name
"1\\"|:"Aard|var|k 
\"{a:1, b:2}\"
Gathorne", |name "2"|:"Sergeant \"Fred\" Squinge", subrecord:{x:{7, 8, 9}, y:{1, 2, {a:{color:"blue"}, b:{{}, 4, {color:"aardvarquene"}, 2, 1}}, 4, 5}, z_:missing value}, d:"47"} -- !
record_labels_to_text(fred)

This old thread describes a variety of ways to extract record labels from Applescript records: shell scripts with regular expressions, vanilla Applescripts, third-party apps including ASObjC Runner and TextCommands, and the List & Record Tools osax.

I would like to present a fairly robust Applescript approach I have been using lately. This approach depends upon the conversion of the record and its property values to their text representations using the forced error message technique. It will fail if that conversion can’t be done, a rare circumstance in my experience. The general approach is simple: (1) convert the record to its text representation, then (2) delimit the text with the text representation of the record’s property values:

on getRecordLabelsAndValues(theRecord)
	script util
		on textValue(v)
			try
				|| of v
			on error m
				set o to offset of "|| of " in m
				return m's text (o + 6) thru -2
			end try
		end textValue
	end script
	set modifiedTextVersionOfRecord to (util's textValue(theRecord)'s text 2 thru -2) & ", "
	set {recordValues, modifiedTextVersionOfRecordValues} to {theRecord's items, {}}
	repeat with v in recordValues
		set end of modifiedTextVersionOfRecordValues to ":" & util's textValue(v's contents) & ", "
	end repeat
	set {tid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, modifiedTextVersionOfRecordValues}
	set recordLabels to modifiedTextVersionOfRecord's text items 1 thru -2
	set AppleScript's text item delimiters to tid
	if recordLabels's length ≠ recordValues's length then error "The record labels could not be parsed because a barred label includes content in the form: colon, then one of the record's property values, then comma, then space; i.e.: \":[record value], \""
	return {recordLabels:recordLabels, recordValues:recordValues}
end getRecordLabelsAndValues

For example:

set theRecord to {a:1, |b "weird 'barred' label" ~!@#$%^&*()_+|:2.2, c:true, d:"hello", e:{1, 2, 3}, f:{g:1, h:2}}
getRecordLabelsAndValues(theRecord)
--> {recordLabels:{"a", "|b \"weird 'barred' label\" ~!@#$%^&*()_+|", "c", "d", "e", "f"}, recordValues:{1, 2.2, true, "hello", {1, 2, 3}, {g:1, h:2}}}

The one outlier circumstance where the method reliably fails is when the record contains a barred label whose content includes a colon, followed by one of the record’s values, followed by a comma, followed by a space.

Thus, this works:

set theRecord to {a:1, b:2, |c :2 |:3}
getRecordLabelsAndValues(theRecord)
--> {recordLabels:{"a", "b", "|c :2 |"}, recordValues:{1, 2, 3}}

But this fails:

set theRecord to {a:1, b:2, |c :2, |:3}
getRecordLabelsAndValues(theRecord)
--> fails because ":2, " fulfills the requirement that a barred label include content of the form: colon, then a record value, then comma, then space

Now data is saved to the clipboard as one record list. Here’s another method.

set the_rec to {a:1, |b:"2"|:3}
set old_clipboard to the clipboard
set the clipboard to the_rec
set mod_rec to list of (the clipboard)
-- reset the clipboard
set the clipboard to old_clipboard
set c to count mod_rec
set the_labels to {}
set the_values to {}
repeat with i from 1 to c by 2
	set end of the_labels to item i of mod_rec
	set end of the_values to item (i + 1) of mod_rec
end repeat
{the_labels, the_values}

gl,
kel

kel1,

I like your method very much…far cleaner than the one I just submitted. I was not aware that one could retrieve a record’s labels and values with the expression

list of (the clipboard)

Has that expression been available since the early days of Applescript, or is it a relative newcomer on the scene?