regulus633 has demonstrated a short method of getting text representations of user record property labels. (If you need to do this, you’re not using AppleScript properly; but the question keeps coming up.)
set theRecord to {firstName:"John", lastName:"Smith"}
tell application "Automator Runner"
set keysList to call method "allKeys" of theRecord
end tell
If the record contains any reserved labels, the script renders them in the returned list as the integer values of their four-byte compiled tokens.
It’s rather irksome having to script an application just for this purpose, especially if you don’t normally have the application open. The time taken to launch it and quit it can make the script quite slow. So here’s a script which uses vanilla methods to the same effect. It uses the File Read/Write commands to write the record to a temporary file and to parse the file’s contents:
on allKeys(rec)
script o
property keyList : {}
-- Parse the temporary file for top-level record labels.
on parseAllLabels(fRef) -- Enter here having skipped "reco" at the beginning of the file.
-- The next four bytes give the number of reserved-label properties in the record. Properties with user labels are grouped togther as a list value with the reserved label "usrf".
repeat (read fRef for 4 as integer) times
set labelKey to (read fRef for 4 as integer) -- Get each reserved label as a 4-byte integer value.
if (labelKey is 1.970500198E+9) then -- If it's "usrf", extract the user label(s).
parseUserLabels(fRef)
else -- Otherwise store the integer and wind on to the next label.
set end of my keyList to labelKey
skipValue(fRef)
end if
end repeat
end parseAllLabels
-- Extract user labels from the "usrf" list.
on parseUserLabels(fRef) -- Enter here having just read "usrf".
read fRef for 4 -- Skip the next four bytes ("list").
-- The four bytes after that give the number of items in the list. The odd-numbered items are the labels and the even-numbered ones the values.
repeat (read fRef for 4 as integer) div 2 times
set labelTextClass to (read fRef for 4 as string)
set labelByteLength to (read fRef for 4 as integer)
if (labelTextClass is "utxt") then -- Unicode text in Snow Leopard (and Leopard?).
set end of my keyList to (read fRef for labelByteLength as Unicode text)
else -- "TEXT" in Tiger.
set end of my keyList to (read fRef for labelByteLength as string)
-- Skip any padding of the label in the file to an even number of bytes.
read fRef for (labelByteLength mod 2)
end if
skipValue(fRef)
end repeat
end parseUserLabels
-- Recursively coax the file mark past the value of a property in the file,
on skipValue(fRef)
set valueClass to (read fRef for 4 as string) -- Get the value class code.
if ((valueClass is "reco") or (valueClass is "list")) then
repeat (read fRef for 4 as integer) times -- Skip each entry in a subordinate record or list.
if (valueClass is "reco") then read fRef for 4 -- Skip the reserved label if in a record.
skipValue(fRef)
end repeat
else
set valueByteLength to (read fRef for 4 as integer)
read fRef for (valueByteLength + valueByteLength mod 2) -- Skip the value and any padding.
end if
end skipValue
end script
-- Open a temporary file to which to write the record and from which to parse it.
set fRef to (open for access file ((path to temporary items as Unicode text) & "Rec.dat") with write permission)
try
set eof fRef to 0
write rec to fRef
read fRef from 5 for 0 -- Set the file mark to 5 to skip the opening "reco".
o's parseAllLabels(fRef) -- Parse the labels.
on error msg
display dialog msg
end try
close access fRef
return o's keyList
end allKeys
set theRecord to {firstName:"John", otherNames:{"Aardvark", "Lazenby"}, lastName:"Smith"}
allKeys(theRecord)
The script advances the file mark past information it doesn’t need by simply reading the information from the file (and then ignoring it). It might be more efficient to maintain an ongoing calculation of the file mark and just read what’s actually needed with the help of 'read’s ‘from’ parameter.
Here’s another way to do it using the “try” block…
local aRecord, recinfo, anitem, tid
set aRecord to {firstName:"John", otherNames:"Aardvark", lastName:"Smith"}
try
aRecord as text
on error recinfo
set recinfo to text 13 thru -18 of recinfo
end try
set tid to text item delimiters
set text item delimiters to ":"
set recinfo to items 1 thru -2 of text items of recinfo
set text item delimiters to tid
repeat with anitem in recinfo
set contents of anitem to last word of anitem
end repeat
return recinfo
Nowadays, if the record’s one dimensional and only has one-word user labels, you could do this:
use framework "Foundation"
on allKeys(rec)
return (current application's class "NSDictionary"'s dictionaryWithDictionary:(rec))'s allKeys() as list
end allKeys
set theRecord to {firstName:"John", otherNames:{"Aardvark", "Lazenby"}, lastName:"Smith"}
allKeys(theRecord)
But Robert’s suggestion also copes with nested records and reserved labels. Not multi-word labels, however. Maybe something like this would do:
use framework "Foundation"
on allKeys(rec)
try
rec as text
on error recinfo
set recinfo to current application's class "NSMutableString"'s stringWithString:(recinfo)
end try
recinfo's replaceOccurrencesOfString:("\"[^\"]*+\"") withString:"" options:(current application's NSRegularExpressionSearch) range:({0, recinfo's |length|()})
set regex to current application's class "NSRegularExpression"'s regularExpressionWithPattern:("(?:\\{|, )([^:\\}]++):") options:(0) |error|:(missing value)
set keyMatches to regex's matchesInString:(recinfo) options:(0) range:({0, recinfo's |length|()})
set theKeys to {}
repeat with thisMatch in keyMatches
set theKeys's end to (recinfo's substringWithRange:(thisMatch's rangeAtIndex:(1))) as text
end repeat
return theKeys
end allKeys
set theRecord to {name:"John", otherNames:{"Aardvark", "Lazenby"}, |last name|:"Smith", nestedList:{1, 2}, nestedRecord:{a:1, b:2}, boobyTrap:"{a:1, b:2}"}
allKeys(theRecord)
I have a script that will parse sub-records also. It is pure AppleScript.
local aRecord, aRecordAsText, theKeys, startChar, endChar, f, p
set aRecord to {otherNames:{mate:{related:"sister"}, animal:{Safer:"Aardvark", Dangerous:"Elephant"}}, lastname:"Banks, Jr", firstName:"John, \"M", myNums:5}
try
set aRecordAsList to aRecord as text
on error aRecordAsText
get aRecordAsText
end try
set aRecordAsText to text 13 thru -18 of aRecordAsText
parseRec(aRecordAsText)
on parseRec(recText)
local theKeys, startChar, endChar, subtext, c, e, f, p, ch, fo, so, d, tid
set startChar to 0
set endChar to startChar
set theKeys to {}
set f to 0 -- skip records and text flag
set ch to ""
set d to 0 -- record depth
set c to 1
repeat until c > length of recText
set p to ch
set ch to text c of recText
if f = 3 then -- inside quote
if ch = "\"" then
if p ≠ "\\" then
set f to 1
end if
end if
else if f = 2 then -- inside record
if ch = "}" then set f to 1
else if f = 1 then -- at key
if ch = "\"" then
set f to 3
else if ch = "{" then -- is sub-list or sub-record
set f to 2
set e to c
set d to d + 1
repeat until d = 0
set subtext to text (e + 1) thru -1 of recText
set fo to offset of "{" in subtext
set so to offset of "}" in subtext
if fo = 0 or so < fo then -- no sub-rec
set d to d - 1
set e to e + so
else
set d to d + 1
set e to e + fo
end if
end repeat
set subtext to text (c + 1) thru (e - 1) of recText
set fo to offset of ":" in subtext
if fo ≠ 0 then -- test if list or record
set so to offset of "}" in subtext
if so = 0 or so > fo then -- is record
set end of theKeys to parseRec(subtext)
set f to 1
end if
end if
set c to e
set ch to text c of recText
else if ch = "," then
set f to 0
end if
else
if ch = "\"" then
set f to 3
else if ch = "{" then
set f to 2
else if ch = ":" then
set end of theKeys to text startChar thru (c - 1) of recText
set f to 1
set startChar to 0
else if startChar = 0 then
if ch = "," then
set f to 3
else if f = 0 and ch ≠ " " then
set startChar to c
end if
end if
end if
set c to c + 1
end repeat
return theKeys
end parseRec
It seems that it should be possible to use ASObjC to work with nested records either with a loop or recursive handler, but I have no idea how to write this. As Nigel has already noted, the following is a simple solution if the record is one-dimensional:
use framework "Foundation"
use scripting additions
set theRecord to {|name|:"John", otherNames:{"Aardvark", "Lazenby"}, |last name|:"Smith", nestedList:{1, 2}, nestedRecord:{a:1, b:2}, boobyTrap:"{a:1, b:2}"}
set theDictionary to current application's NSDictionary's dictionaryWithDictionary:theRecord
set theKeys to (theDictionary's allKeys()) as list --> {"nestedRecord", "otherNames", "last name", "boobyTrap", "name", "nestedList"}
Here is a recursive handler, although the conversion to NSDictionary doesn’t like using properties for the record keys:
use framework "Foundation"
use scripting additions
set theRecord to {|name|:"John", otherNames:{"Aardvark", "Lazenby"}, |last name|:"Smith", nestedList:[1, 2], nestedRecord:{a:{one:"one", two:{three:"three", four:"four"}}, b:2}, boobyTrap:"{a:1, b:2}"}
getKeys from theRecord -- with hierarchyText
# Record keys that are properties or otherwise illegal (spaces, etc) must be escaped with |pipes|.
to getKeys from (dictionary as record) given hierarchyText:(hierarchyText as boolean) : false
set results to {}
set dictionary to current application's NSDictionary's dictionaryWithDictionary:dictionary
set theKeys to (dictionary's allKeys())'s sortedArrayUsingSelector:"localizedStandardCompare:"
repeat with aKey in theKeys
set aKey to aKey as text
set object to (dictionary's objectForKey:aKey)
if (current application's NSStringFromClass(object's |class|()) as text) contains "Dictionary" then
set theKeys to (getKeys from object given hierarchyText:hierarchyText)
if hierarchyText then -- include item hierarchy
set end of results to aKey
repeat with anItem in theKeys
set end of results to (anItem as text) & " of " & aKey
end repeat
else
set end of results to aKey
set end of results to theKeys
-- or --
-- set end of results to {aKey, theKeys}
end if
else
set end of results to aKey
end if
end repeat
return results
end getKeys
Here’s another ASObjC effort. The logic’s similar to red_menace’s, but is differently coded. There’s also an effort to perform with records nested in list values.
use framework "Foundation"
use scripting additions
set aRecord to {otherNames:{mate:{related:"sister"}, animal:{Safer:"Aardvark", Dangerous:"Elephant"}}, listContainingrecords:{1, 2, {a:1, b:2}, 3, {c:1, d:2}, "Hello"}, lastname:"Banks, Jr", firstName:"John, \"M", myNums:5}
return allLabels(aRecord)
on allLabels(theRecord)
script o
property dict : current application's class "NSDictionary"'s dictionaryWithDictionary:(theRecord)
property output : {}
on recurse(currDict, parentList)
set theKeys to currDict's allKeys()
set theValues to currDict's objectsForKeys:(theKeys) notFoundMarker:(null) -- AS null, not ObjC null.
set subList to {}
set parentList's end to subList
repeat with i from 1 to (count theKeys)
set subList's end to (theKeys's item i) as text
set thisValue to theValues's item i
if (thisValue's isKindOfClass:(current application's class "NSDictionary")) then
recurse(thisValue, subList)
else if (thisValue's isKindOfClass:(current application's class "NSArray")) then
set subsublist to {}
set end of subList to subsublist
repeat with listedValue in thisValue
if (listedValue's isKindOfClass:(current application's class "NSDictionary")) then recurse(listedValue, subsublist)
end repeat
end if
end repeat
end recurse
end script
o's recurse(o's dict, o's output)
return o's output's beginning
end allLabels
use framework "Foundation"
use scripting additions
set aRecord to {otherNames:{mate:{related:"sister"}, animal:{Safer:"Aardvark", Dangerous:"Elephant"}}, listContainingrecords:{1, 2, {a:1, b:2}, 3, {c:1, d:{4, 5, {e:missing value}}}, "Hello"}, lastname:"Banks, Jr", firstName:"John, \"M", myNums:5}
return allLabels(aRecord)
on allLabels(theRecord)
script o
property dict : current application's class "NSDictionary"'s dictionaryWithDictionary:(theRecord)
property output : {}
on recurse(currDict, parentText)
set theKeys to currDict's allKeys()
set theValues to currDict's objectsForKeys:(theKeys) notFoundMarker:(null) -- AS null, not ObjC null.
repeat with i from 1 to (count theKeys)
set newParentText to (theKeys's item i as text) & parentText
set my output's end to newParentText
set thisValue to theValues's item i
if (thisValue's isKindOfClass:(current application's class "NSDictionary")) then
recurse(thisValue, " of " & newParentText)
else if (thisValue's isKindOfClass:(current application's class "NSArray")) then
set recordCounter to 0
repeat with listedValue in thisValue
if (listedValue's isKindOfClass:(current application's class "NSDictionary")) then
set recordCounter to recordCounter + 1
recurse(listedValue, " of record " & recordCounter & " in list value of " & newParentText)
end if
end repeat
end if
end repeat
end recurse
end script
o's recurse(o's dict, "")
return o's output
end allLabels