Extracting labels and values from Applescript and application records

Extracting labels from records has been discussed extensively in previous posts. The following is a partial list of proposed solutions: (1) processing the record through the clipboard, (2) processing the record through the Automator Runner application, (3) extracting the labels after coercing the record to an NSDictionary via Cocoa Applescript, (4) osax solutions such as the get user property names command of Late Night Software’s List & Record Tools, (5) parsing the text representation of the record, and (6) the elegant method described by Nigel Garvey of directly parsing the content of the record at the byte level (http://macscripter.net/viewtopic.php?id=35884). Some methods (#'s 1, 2, and 4) have problems with labels that are reserved Applescript words. To my knowledge, all of the methods except #5 are unable to extract labels from application records. Previously described solutions of type #5 have exhibited some difficulties handling certain “challenging” records: those with complex nesting of lists and records and those whose labels and/or values, after transformation into text form, contain the record property delimiter string ", " and/or the label:value delimiter character “:”.

The current handler is an attempt at a vanilla Applescript solution of type #5. It proceeds through the following steps: (1) generates a text representation of the record, (2) tokenizes all text elements enclosed in double-quotes, pipes, and curly braces (i.e., those elements that may contain ", " or “:” character sequences), (3) extracts the record labels and values from the tokenized text, and (4) detokenizes the labels and values. Detokenizing is done recursively to handle any nested tokens. The record’s text representation is gotten with the well known technique of parsing a forced error message. The current code is a bit more complex than customary in order to handle extremely rare outlier cases where simpler methods of parsing the forced error message have failed. Text item delimiters are used for string searching and replacing. Thanks to text item delimiter’s capabilities, the handler executes reasonably quickly.

A known limitation of the handler is an inability to properly parse a record with a piped record label that contains an odd number of unescaped double-quote characters. This should be a rare occurrence in real-life usage. Another limitation affects only the extraction of record values and not record labels. It is encountered when the forced error message technique renders an application record value into a text form that does not permit the reconstruction of the original value with a command of the form: run script “tell application "[application name]" to return [text representation of record value]”. This problem has been encountered very infrequently and only with application values of isolated non-Applescript classes.

A principal goal with the current handler is the extraction of application record labels and values. Yet only a tiny subset of applications have been tested. Any feedback from failure of the handler in the wide world of applications out there, and any comments or suggestions about the handler in general, are most welcome.

USAGE NOTES:

The handler takes as input an Applescript or application record. It returns an Applescript record with the following properties:

[b]recordLabels[/b] - list of the record's labels as text strings
[b]recordLabelsPiped[/b] - list the record's labels as text strings with enclosing pipes for those labels requiring pipes to be valid (those not requiring pipes are rendered without pipes and are identical to the corresponding [b]recordLabels[/b] items)
[b]recordValuesAsText[/b] - list of the text representations of the record's values corresponding to the labels in [b]recordLabels[/b] and [b]recordLabelsPiped[/b]
[b]recordValues[/b] - list of the record's original values corresponding to the labels in [b]recordLabels[/b] and [b]recordLabelsPiped[/b]

The handler uses the Unicode code point 60000 character in the formation of tokens. That character was chosen for its obscurity and low likelihood of appearing in an input record. In the rare circumstance where it does appear, the delimiting character may be changed to any non-conflicting character or character string of the user’s choice by changing the value of the util script’s tokenChar property.

EXAMPLES:

Vanilla Applescript record example:

(Includes labels consisting of Applescript reserved words and pipes without content, and labels and values containing “challenging” text elements: nested lists and records, double-quotes, curly braces, and ", " and “:” character sequences)

set theRecord to {name:1, item:2.2, ww:"foo", xx:true, yy:(current date), zz:{2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}, |""|:true, ||:false, |1 " 2 " 3|:true, |, |:", ", |:|:":", |{a:1, b:2, c:3}|:"{a:1, b:2, c:3}"}
tell recordLabelsAndValues(theRecord)
	its recordLabels --> {"name", "item", "ww", "xx", "yy", "zz", "\"\"", "", "1 \" 2 \" 3", ", ", ":", "{a:1, b:2, c:3}"}
	its recordLabelsPiped --> {"name", "item", "ww", "xx", "yy", "zz", "|\"\"|", "||", "|1 \" 2 \" 3|", "|, |", "|:|", "|{a:1, b:2, c:3}|"}
	its recordValuesAsText --> {"1", "2.2", "\"foo\"", "true", "date \"[...date...]\"", "{2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}", "true", "false", "true", "\", \"", "\":\"", "\"{a:1, b:2, c:3}\""}
	its recordValues --> {1, 2.2, "foo", true, date "[...date...]", {2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}, true, false, true, ", ", ":", "{a:1, b:2, c:3}"}
end tell

Application record example:

tell application "Finder" to set theRecord to properties of desktop
tell recordLabelsAndValues(theRecord) -->
	its recordLabels --> {"class", "name", "index", "displayed name", "name extension", "extension hidden", "container", "disk", "position", "desktop position", "bounds", "kind", "label index", "locked", "description", "comment", "size", "physical size", "creation date", "modification date", "icon", "URL", "owner", "group", "owner privileges", "group privileges", "everyones privileges", "container window"}
	its recordLabelsPiped --> {"class", "name", "index", "displayed name", "name extension", "extension hidden", "container", "disk", "position", "desktop position", "bounds", "kind", "label index", "locked", "description", "comment", "size", "physical size", "creation date", "modification date", "icon", "URL", "owner", "group", "owner privileges", "group privileges", "everyones privileges", "container window"}
	its recordValuesAsText --> {"desktop-object", "\"Desktop\"", "missing value", "\"Desktop\"", "\"\"", "false", "folder \"[...username...]\" of folder \"Users\" of startup disk of application \"Finder\"", "startup disk of application \"Finder\"", "{-1, -1}", "missing value", "{-33, -33, 31, 31}", "\"Desktop\"", "0", "false", "missing value", "\"\"", "4.3351161719E+10", "4.3355721728E+10", "date \"[...date...]\"", "date \"[...date...]\"", "missing value", "\"file:///Users/[...username...]/Desktop/\"", "\"[...username...]\"", "\"(unknown)\"", "read write", "none", "none", "window of desktop of application \"Finder\""}
	its recordValues --> {desktop-object, "Desktop", missing value, "Desktop", "", false, folder "[...username...]" of folder "Users" of startup disk of application "Finder", startup disk of application "Finder", {-1, -1}, missing value, {-33, -33, 31, 31}, "Desktop", 0, false, missing value, "", 4.3351161719E+10, 4.3355721728E+10, date "[...date...]", date "[...date...]", missing value, "file:///Users/[...username...]/Desktop/", "[...username...]", "(unknown)", read write, none, none, window of desktop of application "Finder"}
end tell

HANDLER:

on recordLabelsAndValues(theRecord)
	-- Returns the unpiped and piped forms of a record's labels and the text and value forms of a record's values
	-- Utility properties and handlers
	script util
		property tokenChar : character id 60000 -- obscure character chosen for the low likelihood of its appearance in an input record's text representation; this may be substituted by any character or character string that does not appear in the text representation of the input record
		property tokenizedStrings : {}
		on detokenizeString(tokenizedString)
			-- Convert any tokens of the form "[token char][token index number][token char]" to their original values; handle nested tokens with recursive handler calls
			set tid to AppleScript's text item delimiters
			try
				set AppleScript's text item delimiters to my tokenChar
				tell (get tokenizedString's text items)
					if length < 3 then
						set originalString to tokenizedString
					else
						set originalString to (item 1) & my detokenizeString((my tokenizedStrings's item ((item 2) as integer)) & (items 3 thru -1))
					end if
				end tell
			end try
			set AppleScript's text item delimiters to tid
			return originalString
		end detokenizeString
		on representValueAsText(theValue)
			-- Parse a forced error message for the text representation of the input value
			try
				|| of {theValue}
			on error m
				try
					if m does not contain "{" then error
					repeat while m does not start with "{"
						set m to m's text 2 thru -1
					end repeat
					if m does not contain "}" then error
					repeat while m does not end with "}"
						set m to m's text 1 thru -2
					end repeat
					if m = "{}" then error
					set valueAsText to m's text 2 thru -2
				on error
					try
						-- Try an alternative method of generating a text representation from a forced error message if the first method fails
						{||:{theValue}} as null
					on error m
						try
							if m does not contain "{" then error
							repeat while m does not start with "{"
								set m to m's text 2 thru -1
							end repeat
							set m to m's text 2 thru -1
							repeat while m does not start with "{"
								set m to m's text 2 thru -1
							end repeat
							if m does not contain "}" then error
							repeat while m does not end with "}"
								set m to m's text 1 thru -2
							end repeat
							set m to m's text 1 thru -2
							repeat while m does not end with "}"
								set m to m's text 1 thru -2
							end repeat
							if m = "{}" then error
							set valueAsText to m's text 2 thru -2
						on error
							error "Can't get a text representation of the value."
						end try
					end try
				end try
			end try
			return valueAsText
		end representValueAsText
	end script
	-- Perform the handler's actions inside a try block to capture any errors; if an error is encountered, restore AppleScript's text item delimiters to their baseline value
	set tid to AppleScript's text item delimiters
	try
		-- Handle the special case of an empty record
		if theRecord = {} then return {recordLabels:{}, recordLabelsPiped:{}, recordValuesAsText:{}, recordValues:{}}
		-- Get the text representation of the input record
		set textValue to util's representValueAsText(theRecord)
		-- Partially validate the text representation, and remove the leading and trailing curly braces
		tell textValue
		-- This test does not exclude an input argument in the form of an Applescript list; however, a list will result in a parsing error in the code below
			if (it does not start with "{") or (it does not end with "}") then error "The input value is not a record."
			set textValue to text 2 thru -2
		end tell
		-- Initialize return values and the token counter
		set {recordLabels, recordLabelsPiped, recordValuesAsText, recordValues, iToken} to {{}, {}, {}, {}, 0}
		-- Tokenize text elements that could potentially contain record property (", ") or label/value (":") delimiter characters in order to avoid errors while parsing record properties
		-- Tokens are of the form "[token char][token index number][token char]", where the index numbers increase sequentially 1, 2, 3, ... with each new token
		-- Tokenize escaped double-quote characters (to facilitate tokenizing double-quoted items)
		set AppleScript's text item delimiters to "\\\""
		tell (get textValue's text items)
			if length > 1 then
				set iToken to iToken + 1
				set AppleScript's text item delimiters to (util's tokenChar) & iToken & (util's tokenChar)
				set {textValue, end of util's tokenizedStrings} to {it as text, "\\\""}
			end if
		end tell
		-- Tokenize double-quoted items
		set AppleScript's text item delimiters to "\""
		tell (get textValue's text items)
			if length > 2 then
				set textValue to ""
				repeat with i from 1 to (length - 2) by 2
					set iToken to iToken + 1
					set {textValue, end of util's tokenizedStrings} to {textValue & (item i) & (util's tokenChar) & iToken & (util's tokenChar), "\"" & item (i + 1) & "\""}
				end repeat
				set textValue to textValue & item -1
			end if
		end tell
		-- Tokenize piped items
		set AppleScript's text item delimiters to "|"
		tell (get textValue's text items)
			if length > 2 then
				set textValue to ""
				repeat with i from 1 to (length - 2) by 2
					set iToken to iToken + 1
					set {textValue, end of util's tokenizedStrings} to {textValue & (item i) & (util's tokenChar) & iToken & (util's tokenChar), "|" & item (i + 1) & "|"}
				end repeat
				set textValue to textValue & item -1
			end if
		end tell
		-- Tokenize curly-braced items
		set AppleScript's text item delimiters to "{"
		tell (get textValue's text items)
			if length > 1 then
				set {AppleScript's text item delimiters, textValue, iNestedLevel, currBracedString} to {"}", item 1, 1, ""}
				repeat with s in rest
					tell (get s's text items) to if length > 1 then set {t, iNestedLevel} to {it, iNestedLevel + 1 - length}
					if iNestedLevel > 0 then
						set {iNestedLevel, currBracedString} to {iNestedLevel + 1, currBracedString & s & "{"}
					else
						set iToken to iToken + 1
						set {textValue, iNestedLevel, currBracedString, end of util's tokenizedStrings} to {textValue & (util's tokenChar) & iToken & (util's tokenChar) & t's item -1, 1, "", "{" & currBracedString & (t's items 1 thru -2) & "}"}
					end if
				end repeat
			end if
		end tell
		-- Get the unpiped and piped record labels and text representations of the record values
		set AppleScript's text item delimiters to ", " -- delimits record properties
		tell (get textValue's text items)
			set AppleScript's text item delimiters to ":" -- delimits the label and value for a given record property
			repeat with s in it
				-- Extract and detokenize the current record label and value
				tell (get s's text items) to set {end of recordLabelsPiped, end of recordValuesAsText} to {util's detokenizeString(item 1), util's detokenizeString(item 2)}
				-- Create an unpiped version of the current record label
				tell recordLabelsPiped's item -1
					if it = "||" then
						set end of recordLabels to ""
					else if (it starts with "|") and (it ends with "|") then
						set end of recordLabels to text 2 thru -2
					else
						set end of recordLabels to it
					end if
				end tell
			end repeat
		end tell
		-- Get the record values
		set recordValues to theRecord as list
	on error m number n
		set AppleScript's text item delimiters to tid
		if n = -128 then error number -128
		if n ≠ -2700 then set m to "(" & n & ") " & m -- -2700 = purposely thrown error
		error "Handler recordLabelsAndValues error:" & return & return & m
	end try
	set AppleScript's text item delimiters to tid
	-- Return the results
	return {recordLabels:recordLabels, recordLabelsPiped:recordLabelsPiped, recordValuesAsText:recordValuesAsText, recordValues:recordValues}
end recordLabelsAndValues

Edit note: The original submission mistakenly described the record in the application record example above as a pseudo-record. It is in fact a conventional record but simply has overridden Applescript’s class property. The term “pseudo” has been removed.

Well that’s handled everything I’ve thrown at it so far! :cool: It only handles top-level labels in nested records, but that’s probably best anyway.

Perhaps you should consider reposting it in Code Exchange, MacScripter’s forum for proffered code. I can ask one of the administrators to move this thread if you like. Or you could repost the code there yourself.

Nigel, thank you for the encouraging testing thus far. I was hoping to get a little feedback from testing with other applications (to make sure it wasn’t a flop!) But at this point, yes, it would be nice if you could kindly ask the administrators to move it to Code Exchange.

For what it’s worth I have found a case where this Applescript fails.

When running this code to evaluate the properties of application Capture One 11, v11.0.1.40 (the latest version) I get the error: (-2753) The variable originalString is not defined." number -2700 from «script» to item

The error occurrs during the recursion of the handler detokenizeString(tokenizedString)

My code is:

tell application "Capture One 11"
	set theproperties to get properties
	log {class of theproperties, count of theproperties}
end tell

tell recordLabelsAndValues(theproperties)
	set ColA to its recordLabels
	set ColB to its recordValuesAsText
end tell

My guess at what is happening is that the Capture One application lists each of it’s images in it’s properties, and this results in one of the records containing a very long list of lists of records. Somewhere in there Applescript maybe running out of stack space. However, I don’t understand why that should trigger this error.

This particular test fails when there are 193 images (a relatively small number)

OSX 10.12.6, I don’t think its a beta version

Model: Late 2015 iMac with 24GB RAM 4GHz i7
AppleScript: 2.4
Browser: Firefox 59.0
Operating System: Mac OS X (10.12.6 beta 6)

Thank you for pointing out a case where the handler fails. An input record with a sufficiently large number of text string elements requiring tokenization could certainly cause the detokenizeString handler to fail by exceeding Applescript’s limits on recursive handler calls. In the following modified version of the handler, the detokenizeString handler now uses nested repeat loops rather than recursive handler calls to detokenize nested tokenized text string elements. That should avoid any problems related to Applescript limitations on recursive handler calls. (Also, the representValueAsText handler that gets the text representation of the input record has been tidied up a bit…same result, but cleaner code.)

It’s worth a try to see if this modified handler works with your application record:


on recordLabelsAndValues(theRecord)
	-- Returns the unpiped and piped forms of a record's labels and the text and value forms of a record's values
	-- Utility properties and handlers
	script util
		property tokenChar : character id 60000 -- obscure character chosen for the low likelihood of its appearance in an input record's text representation; this may be substituted by any character or character string that does not appear in the text representation of the input record
		property tokenizedStrings : {}
		property originalString : null
		property tempString : null
		on detokenizeString(tokenizedString)
			-- Convert any tokens of the form "[token char][token index number][token char]" to their original values; handle nested tokens with nested repeat loops
			set tid to AppleScript's text item delimiters
			try
				set AppleScript's text item delimiters to my tokenChar
				set my originalString to tokenizedString's text items
				repeat while my originalString's length > 2
					set {my tempString, i} to {{}, 0}
					repeat with j in my originalString
						set i to i + 1
						if i mod 2 = 0 then
							set end of my tempString to my tokenizedStrings's item (j as integer)
						else
							set end of my tempString to (j as text)
						end if
					end repeat
					set AppleScript's text item delimiters to ""
					set my tempString to my tempString as text
					set AppleScript's text item delimiters to my tokenChar
					set my originalString to my tempString's text items
				end repeat
			end try
			set AppleScript's text item delimiters to tid
			return my originalString as text
		end detokenizeString
		on representValueAsText(theValue)
			-- Parse a forced error message for the text representation of the input value
			try
				|| of {theValue}
			on error m
				set tid to AppleScript's text item delimiters
				try
					set AppleScript's text item delimiters to "{"
					set m to m's text items 2 thru -1 as text
					set AppleScript's text item delimiters to "}"
					set valueAsText to m's text items 1 thru -2 as text
				on error
					try
						-- Try an alternative method of generating a text representation from a forced error message if the first method fails
						{||:{theValue}} as null
					on error m
						try
							set AppleScript's text item delimiters to "{"
							set m to m's text items 3 thru -1 as text
							set AppleScript's text item delimiters to "}"
							set valueAsText to m's text items 1 thru -3 as text
						on error
							try
								-- Try a third method of generating a text representation from a forced error message if the first two method fails
								script
									error theValue
								end script
								run script result
							on error m
								set valueAsText to m
							end try
						end try
					end try
				end try
				set AppleScript's text item delimiters to tid
			end try
			return valueAsText
		end representValueAsText
	end script
	-- Perform the handler's actions inside a try block to capture any errors; if an error is encountered, restore AppleScript's text item delimiters to their baseline value
	set tid to AppleScript's text item delimiters
	try
		-- Handle the special case of an empty record
		if theRecord = {} then return {recordLabels:{}, recordLabelsPiped:{}, recordValuesAsText:{}, recordValues:{}}
		-- Get the text representation of the input record
		set textValue to util's representValueAsText(theRecord)
		-- Partially validate the text representation, and remove the leading and trailing curly braces
		tell textValue
			-- This test does not exclude an input argument in the form of an Applescript list; however, a list will result in a parsing error in the code below
			if (it does not start with "{") or (it does not end with "}") then error "The input value is not a record."
			set textValue to text 2 thru -2
		end tell
		-- Initialize return values and the token counter
		set {recordLabels, recordLabelsPiped, recordValuesAsText, recordValues, iToken} to {{}, {}, {}, {}, 0}
		-- Tokenize text elements that could potentially contain record property (", ") or label/value (":") delimiter characters in order to avoid errors while parsing record properties
		-- Tokens are of the form "[token char][token index number][token char]", where the index numbers increase sequentially 1, 2, 3, ... with each new token
		-- Tokenize escaped double-quote characters (to facilitate tokenizing double-quoted items)
		set AppleScript's text item delimiters to "\\\""
		tell (get textValue's text items)
			if length > 1 then
				set iToken to iToken + 1
				set AppleScript's text item delimiters to (util's tokenChar) & iToken & (util's tokenChar)
				set {textValue, end of util's tokenizedStrings} to {it as text, "\\\""}
			end if
		end tell
		-- Tokenize double-quoted items
		set AppleScript's text item delimiters to "\""
		tell (get textValue's text items)
			if length > 2 then
				set textValue to ""
				repeat with i from 1 to (length - 2) by 2
					set iToken to iToken + 1
					set {textValue, end of util's tokenizedStrings} to {textValue & (item i) & (util's tokenChar) & iToken & (util's tokenChar), "\"" & item (i + 1) & "\""}
				end repeat
				set textValue to textValue & item -1
			end if
		end tell
		-- Tokenize piped items
		set AppleScript's text item delimiters to "|"
		tell (get textValue's text items)
			if length > 2 then
				set textValue to ""
				repeat with i from 1 to (length - 2) by 2
					set iToken to iToken + 1
					set {textValue, end of util's tokenizedStrings} to {textValue & (item i) & (util's tokenChar) & iToken & (util's tokenChar), "|" & item (i + 1) & "|"}
				end repeat
				set textValue to textValue & item -1
			end if
		end tell
		-- Tokenize curly-braced items
		set AppleScript's text item delimiters to "{"
		tell (get textValue's text items)
			if length > 1 then
				set {AppleScript's text item delimiters, textValue, iNestedLevel, currBracedString} to {"}", item 1, 1, ""}
				repeat with s in rest
					tell (get s's text items) to if length > 1 then set {t, iNestedLevel} to {it, iNestedLevel + 1 - length}
					if iNestedLevel > 0 then
						set {iNestedLevel, currBracedString} to {iNestedLevel + 1, currBracedString & s & "{"}
					else
						set iToken to iToken + 1
						set {textValue, iNestedLevel, currBracedString, end of util's tokenizedStrings} to {textValue & (util's tokenChar) & iToken & (util's tokenChar) & t's item -1, 1, "", "{" & currBracedString & (t's items 1 thru -2) & "}"}
					end if
				end repeat
			end if
		end tell
		-- Get the unpiped and piped record labels and text representations of the record values
		set AppleScript's text item delimiters to ", " -- delimits record properties
		tell (get textValue's text items)
			set AppleScript's text item delimiters to ":" -- delimits the label and value for a given record property
			repeat with s in it
				-- Extract and detokenize the current record label and value
				tell (get s's text items) to set {end of recordLabelsPiped, end of recordValuesAsText} to {util's detokenizeString(item 1), util's detokenizeString(item 2)}
				-- Create an unpiped version of the current record label
				tell recordLabelsPiped's item -1
					if it = "||" then
						set end of recordLabels to ""
					else if (it starts with "|") and (it ends with "|") then
						set end of recordLabels to text 2 thru -2
					else
						set end of recordLabels to it
					end if
				end tell
			end repeat
		end tell
		-- Get the record values
		set recordValues to theRecord as list
	on error m number n
		set AppleScript's text item delimiters to tid
		if n = -128 then error number -128
		if n ≠ -2700 then set m to "(" & n & ") " & m -- -2700 = purposely thrown error
		error "Handler recordLabelsAndValues error:" & return & return & m
	end try
	set AppleScript's text item delimiters to tid
	-- Return the results
	return {recordLabels:recordLabels, recordLabelsPiped:recordLabelsPiped, recordValuesAsText:recordValuesAsText, recordValues:recordValues}
end recordLabelsAndValues

Edit note: A minor “dressing” change was made to the detokenizeString handler that will not alter its behavior.
Edit note: A typo was corrected in the “Try a third method…” section of the representValueAsText handler.

Thanks!! I will give it a try.

Thank you very much! Not only does it run without issues, it runs a lot faster than the previous version.

Great news! Thanks for your input that helped to shore up a weak spot in the handler.

(P.S. A typo was corrected in the “Try a third method…” section of the representValueAsText handler that would likely never have been encountered since the 1st or 2nd method should virtually always generate a text representation successfully. It is fixed nonetheless.)

The following is an updated, alternative handler for extracting record labels and values that is recommended over the one posted above. It has the advantage of properly processing the outlier case discussed in the initial post that the previously posted handler could not handle, namely, a piped record label containing an odd number of unescaped literal double-quote characters within the pipes. In fact, the current handler should be able to handle every conceivable AppleScript and application record. It is also a simpler solution than the tokenization approach taken above. It instead leverages the observation that the text representations (by the forced error technique) of individual record values extracted from a record by the as list coercion command appear in the same order and with the identical text content as they do in the text representation of the record as a whole. The record value text representations can then facilitate the extraction of record labels by serving as text item delimiters. Handler execution speed is quite good, about the same as that of the previously presented handler. For example, the handler takes approximately ~0.002 seconds (or less on a faster machine) to process the vanilla AppleScript record shown in the example below, and ~0.15 seconds to process a record of 450 complex record properties with “challenging” text patterns and content and deeply nested list and record values and piped labels (not shown.)

Here is an example with a vanilla Applescript record, including labels and values with “challenging” text patterns and content (Applescript reserved words, piped label without text content, values and piped labels containing nested lists and records, double-quotes, curly braces, and “, " and “:” character sequences; also included are two labels containing an odd number of unescaped double-quote characters within pipes, namely, |”| and |1 " 2 " 3 " 4|, that cannot be handled properly by the previously posted handler):


set theRecord to {name:1, item:2.2, ww:"foo", xx:true, yy:(current date), zz:{2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}, |"|:true, ||:false, |1 " 2 " 3 " 4|:true, |, |:", ", |:|:":", |{a:1, b:2, c:3}|:"{a:1, b:2, c:3}"}

recordLabelsAndValues(theRecord) -->

	recordLabels --> {"name", "item", "ww", "xx", "yy", "zz", "\"", "", "1 \" 2 \" 3 \" 4", ", ", ":", "{a:1, b:2, c:3}"}
	recordLabelsPiped --> {"name", "item", "ww", "xx", "yy", "zz", "|\"|", "||", "|1 \" 2 \" 3 \" 4|", "|, |", "|:|", "|{a:1, b:2, c:3}|"}
	recordValuesAsText --> {"1", "2.2", "\"foo\"", "true", "date \"[date]\"", "{2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}", "true", "false", "true", "\", \"", "\":\"", "\"{a:1, b:2, c:3}\""}
	recordValues --> {1, 2.2, "foo", true, date "[date]", {2, {aa:{3, {4, 5}, {bb:{6, {7, 8}, {cc:9, dd:10}}, ee:11}}, ff:12}}, true, false, true, ", ", ":", "{a:1, b:2, c:3}"}

Here is an example with an application record:


tell application "Finder" to set theRecord to properties of desktop

recordLabelsAndValues(theRecord) -->

	recordLabels --> {"class", "name", "index", "displayed name", "name extension", "extension hidden", "container", "disk", "position", "desktop position", "bounds", "kind", "label index", "locked", "description", "comment", "size", "physical size", "creation date", "modification date", "icon", "URL", "owner", "group", "owner privileges", "group privileges", "everyones privileges", "container window"}
	recordLabelsPiped --> {"class", "name", "index", "displayed name", "name extension", "extension hidden", "container", "disk", "position", "desktop position", "bounds", "kind", "label index", "locked", "description", "comment", "size", "physical size", "creation date", "modification date", "icon", "URL", "owner", "group", "owner privileges", "group privileges", "everyones privileges", "container window"}
	recordValuesAsText --> {"desktop-object", "\"Desktop\"", "missing value", "\"Desktop\"", "\"\"", "false", "folder \"[username]\" of folder \"Users\" of startup disk of application \"Finder\"", "startup disk of application \"Finder\"", "{224, 1636}", "missing value", "{192, 1604, 256, 1668}", "\"Desktop\"", "0", "false", "missing value", "\"\"", "1.357642978E+9", "1.358168064E+9", "date \"[date]\"", "date \"[date]\"", "missing value", "\"file:///Users/[username]/Desktop/\"", "\"[username]\"", "\"(unknown)\"", "read write", "none", "none", "window of desktop folder of application \"Finder\""}
	recordValues --> {desktop-object, "Desktop", missing value, "Desktop", "", false, folder "[username]" of folder "Users" of startup disk, startup disk, {224, 1636}, missing value, {192, 1604, 256, 1668}, "Desktop", 0, false, missing value, "", 1.357642978E+9, 1.358168064E+9, date "[date]", date "[date]", missing value, "file:///Users/[username]/Desktop/", "[username]", "(unknown)", read write, none, none, window of desktop folder}

Here is the handler:


on recordLabelsAndValues(theRecord)
	-- Returns the unpiped and piped forms of a record's labels and the text and value forms of a record's values
	-- Utility handler
	script util
		property recordLabels : {}
		property recordLabelsPiped : {}
		property recordValuesAsText : {}
		property recordValues : theRecord as list
		on representValueAsText(theValue)
			-- Parse a forced error message for the text representation of the input value
			try
				|| of {theValue}
			on error m
				set tid to AppleScript's text item delimiters
				try
					set AppleScript's text item delimiters to "{"
					set m to m's text items 2 thru -1 as text
					set AppleScript's text item delimiters to "}"
					set valueAsText to m's text items 1 thru -2 as text
				on error
					try
						-- Try an alternative method of generating a text representation from a forced error message if the first method fails
						{||:{theValue}} as null
					on error m
						try
							set AppleScript's text item delimiters to "{"
							set m to m's text items 3 thru -1 as text
							set AppleScript's text item delimiters to "}"
							set valueAsText to m's text items 1 thru -3 as text
						on error
							error "Could not get a text representation of the input value."
						end try
					end try
				end try
				set AppleScript's text item delimiters to tid
			end try
			return valueAsText
		end representValueAsText
	end script
	-- Perform the handler's actions inside a try block to capture any errors
	try
		set tid to AppleScript's text item delimiters
		-- Get the text representation of the input record
		set serializedRecord to util's representValueAsText(theRecord)
		-- Validate the text representation
		tell serializedRecord
			-- This test does not flag an input argument in the form of an Applescript list; however, a list will result in a parsing error in the code below
			if (it does not start with "{") or (it does not end with "}") then error "The input value is not a record."
			-- Handle the special case of an empty record
			if it = "{}" then return {recordLabels:{}, recordLabelsPiped:{}, recordValuesAsText:{}, recordValues:{}}
		end tell
		-- Remove the leading and trailing curly braces, and add a trailing ", " to the last record property to mimic the text pattern of the remaining properties
		set currSerializedRecord to (serializedRecord's text 2 thru -2) & ", "
		-- Parse the input record properties individually from first to last, using the text representation of the individual record values as text item delimiters
		repeat with currValue in util's recordValues
			-- Get the text representation of the current record property value
			set currSerializedValue to util's representValueAsText(currValue's contents)
			-- Consider the current record property label to be piped if the first character is a pipe
			set isPipedLabel to (currSerializedRecord starts with "|")
			-- Set the text item delimiter string to a colon (i.e., label-value separator), followed by the text representation of the record property value, followed by comma-space (i.e., property separator), optionally prefixing the entire string with a pipe if the label is piped
			if isPipedLabel then
				set currDelimiter to "|:" & currSerializedValue & ", "
				set invalidLabelChar to "|"
				set currSerializedRecord to currSerializedRecord's text 2 thru -1 -- "...text 2..." removes the leading pipe character
			else
				set currDelimiter to ":" & currSerializedValue & ", "
				set invalidLabelChar to ":"
			end if
			-- Extract the unpiped and piped versions of the current record property label
			set AppleScript's text item delimiters to currDelimiter
			tell (get currSerializedRecord's text items)
				-- Flag the input item as a non-record if (1) the text item delimiter string isn't found, (2) the property "label" contains an invalid character, or (3) the "label" is missing
				if (length = 1) or (item 1 contains invalidLabelChar) or (not isPipedLabel and (item 1 = "")) then error "The input argument theRecord does not appear to be a valid AppleScript record."
				-- Set the current record property label to the first text item, and the remaining record (still to be parsed) to the text beginning with the second text item (this will be the empty string if the current record property is the last property)
				set {currLabel, currLabelPiped} to {item 1, item 1}
				if isPipedLabel then set currLabelPiped to "|" & currLabelPiped & "|"
				set currSerializedRecord to rest as text
			end tell
			-- Save the results for the current record property
			set {end of util's recordLabels, end of util's recordLabelsPiped, end of util's recordValuesAsText} to {currLabel, currLabelPiped, currSerializedValue}
		end repeat
		if currSerializedRecord ≠ "" then error "The input argument theRecord does not appear to be a valid AppleScript record."
		-- Restore AppleScript's text item delimiters to their baseline value
		set AppleScript's text item delimiters to tid
		-- Return the results
		return {recordLabels:util's recordLabels as list, recordLabelsPiped:util's recordLabelsPiped as list, recordValuesAsText:util's recordValuesAsText as list, recordValues:util's recordValues as list}
	on error m number n
		set AppleScript's text item delimiters to tid
		if n = -128 then error number -128
		if n ≠ -2700 then set m to "(" & n & ") " & m -- -2700 = purposely thrown error
		error "Handler recordLabelsAndValues error:" & return & return & m
	end try
end recordLabelsAndValues

Edit note: The handler was modified slightly shortly after being posted such that the return values are now in the form of script properties in order to enhance execution speed in the case of a very large number of record properties.

1 Like