Tuesday, December 12, 2017
  • Index
  •  » Code Exchange
  •  » Extracting labels and values from Applescript and application records

#1 2017-01-02 01:22:33 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

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:

    recordLabels - list of the record's labels as text strings
    recordLabelsPiped - 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 recordLabels items)
    recordValuesAsText - list of the text representations of the record's values corresponding to the labels in recordLabels and recordLabelsPiped
    recordValues - list of the record's original values corresponding to the labels in recordLabels and recordLabelsPiped

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)

Applescript:

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:

Applescript:

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:

Applescript:

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.

Last edited by bmose (2017-01-04 12:05:15 pm)

Offline

 

#2 2017-01-02 08:15:47 am

Nigel Garvey
Moderator
From:: Warwickshire, England
Registered: 2002-11-20
Posts: 4451

Re: Extracting labels and values from Applescript and application records

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.


NG

Offline

 

#3 2017-01-02 08:40:30 am

bmose
Member
From:: Massachusetts
Registered: 2006-01-03
Posts: 219

Re: Extracting labels and values from Applescript and application records

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.

Offline

 
  • Index
  •  » Code Exchange
  •  » Extracting labels and values from Applescript and application records

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)