I am working on an application that stores its settings in a plist using “defaults read” and “defaults write” in a do shell script. This has been working great but now I need to store and retrieve an array of values.
Searching this forum I found that this should be possible, I also found other examples that show the array flag in use in the defaults command, which sent me looking at the man page for the defaults command.
I guess my question is now how do I format a list so that I can pass it through to defaults and also read an array into a list from defaults?
set someList to {"entry 1", "entry 2"}
do shell script "defaults write com.blah someEntry -array " & someList
EDIT - Okay the read mechanism is really really annoying when it returns an array. This will currently only work when your elements have a space in them.
set writeList to {"entry 3", "entry 4"}
writeArray("com.testscript.wplate", "testArray", writeList)
set someList to readArray("com.testscript.wplate", "testArray")
on writeArray(theDomain, theKey, theList)
set theArrayList to ""
repeat with anItem in theList
set theArrayList to theArrayList & "'" & anItem & "'" & space
end repeat
set theArrayList to text 1 thru -2 of theArrayList
set command to "/usr/bin/defaults write " & theDomain & space & theKey & " -array " & theArrayList
do shell script command
end writeArray
on readArray(theDomain, theKey)
set command to "/usr/bin/defaults read " & theDomain & space & theKey
set {tid, text item delimiters} to {text item delimiters, "\""}
set rawList to text items of (do shell script command)
set varList to {}
repeat with i from 2 to (count rawList) by 2
set end of varList to item i of rawList
end repeat
set text item delimiters to tid
return varList
end readArray
Resultingly when the script counts by two later on I’m left with just one item in my list.
I’m sure I’ve done something wrong but I I’ve stepped through in debugging mode like ten times and I’m not seeing my mistake. Can anyone else point out what I’ve done wrong?
on readArray(theDomain, theKey)
set command to "/usr/bin/defaults read " & theDomain & space & theKey
set arrayItems to text 2 thru -2 of (do shell script command) -- Lose the enclosing brackets.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "\""
set unquotedQuoted to arrayItems's text items
-- 'unquotedQuoted is a list whose even-numbered items (if any) were quoted in the returned array text.
-- The odd-numbered items are either empty texts, comma-spaces, or blocks of unquoted array text.
set outList to {}
set AppleScript's text item delimiters to ", "
repeat with i from 1 to (count unquotedQuoted)
if (i mod 2 is 1) then -- Odd number: parse out unquoted array items.
set unquotedItems to text items of item i of unquotedQuoted
repeat with j from 1 to (count unquotedItems)
set thisItem to item j of unquotedItems
if ((count thisItem) > 0) then set end of outList to thisItem
end repeat
else -- Even number: this was a quoted array item.
set end of outList to item i of unquotedQuoted
end if
end repeat
set AppleScript's text item delimiters to astid
return outList
end readArray
Nigel, I didn’t really look at the code yet, but here was my quick test using your read function
set writeList to {"entry 3", "entry 4", "entry5"}
writeArray("com.testscript.wplate", "testArray", writeList)
set someList to readArray("com.testscript.wplate", "testArray")
Okay took another stab at this and here is what I came up with. Give this a try
set writeList to {"entry 3", "entry4", "entry 8", "entry5", "entry 6 7", "entry 7 8 9", "entry"}
writeArray("com.testscript.wplate", "testArray", writeList)
set someList to readArray("com.testscript.wplate", "testArray")
on writeArray(theDomain, theKey, theList)
set theArrayList to ""
repeat with anItem in theList
set theArrayList to theArrayList & "'" & anItem & "'" & space
end repeat
set theArrayList to text 1 thru -2 of theArrayList
set command to "/usr/bin/defaults write " & theDomain & space & theKey & " -array " & theArrayList
do shell script command
end writeArray
on readArray(theDomain, theKey)
set command to "/usr/bin/defaults read " & theDomain & space & theKey
set rawList to paragraphs of (text 3 thru -3 of (do shell script command))
set outList to {}
repeat with i from 1 to (count rawList) - 1
set tmpItem to text 5 thru -2 of item i of rawList
if tmpItem's character 1 is "\"" then set tmpItem to text 2 thru -2 of tmpItem
set end of outList to tmpItem
end repeat
set tmpItem to text 5 thru -1 of item -1 of rawList
if tmpItem's character 1 is "\"" then set tmpItem to text 2 thru -2 of tmpItem
set end of outList to tmpItem
return outList
end readArray
I have an AppleScript Studio project that does exactly what you are trying to do, save a table basically that can be read in from an external file and edited and then saved back out.
I did this by saving the data in an XML file rather than the pList file. If you have any experience with XML I think that is the best way to go about it. I use System Events to read the XML data in and then use the free Late Night Software XML scripting addition to write out the new XML file at the end.
Thank you, Matt-Boy for the XML suggestion, I probably would have gone that route had it not been easy to read and write basic settings using defaults. Then when I realized I needed an array this thread started.
James Nierodzik’s last example does seem to do what I need it to do (at least for now), so I’ll give it a go.
XML file for doing find and replace of colors in InDesign looks like this:
<?xml version="1.0"?>
Black Variable
Black
Variable Text
Black
White Variable
Paper
And the Applescript. In between the reading in of the values and the writing out other stuff happens in my XCode project, but this is just the read and write routines with the sorting routine that keeps the list in alphabetical order.
set configurationPath to path to desktop as text
set XMLfile to configurationPath & "Colors_Find_Replace.xml"
set colorList to {}
set theColorListTemp4Sorting to {}
tell application "System Events"
tell XML element 1 of contents of XML file XMLfile --tell <colors> block
set colorListTotal to (count of XML elements)
--Pull find and replace color values out of XML file and create a record list.
repeat with thisElement from 1 to colorListTotal
set colorListTempFind to (value of (XML elements whose name is "findColor") of XML element thisElement) as string
set colorListTempReplace to (value of (XML elements whose name is "replaceColor") of XML element thisElement) as string
set theColorListTemp4Sorting to theColorListTemp4Sorting & {{findColorValue:colorListTempFind, replaceColorValue:colorListTempReplace}}
end repeat
--Sort list alphabetically by find color to create the final colorList
set colorList to my bubbleSortPairedList(theColorListTemp4Sorting)
end tell
end tell
--Write out new XML file with updated color list
set theFinalXMLParamsList to {}
repeat with thisColorRecord in colorList
set theFinalXMLParamsList to theFinalXMLParamsList & {{class:XML element, XML tag:"color", XML contents:{{class:XML element, XML tag:"findColor", XML contents:(findColorValue of thisColorRecord)}, {class:XML element, XML tag:"replaceColor", XML contents:(replaceColorValue of thisColorRecord)}}}}
end repeat
--Set up the parameters for the XML Tools "generate XML" function
set theXML to {class:XML element, XML tag:"colors", XML contents:theFinalXMLParamsList}
--Generate the new XML and write it over the existing file
generate XML theXML saving as file (configurationPath & "Colors_Find_Replace.xml") without pretty printing
on bubbleSortPairedList(theList2Sort)
repeat with i from length of theList2Sort to 2 by -1
repeat with j from 1 to i - 1
tell theList2Sort
if ((item j)'s findColorValue) > ((item (j + 1))'s findColorValue) then
set {item j, item (j + 1)} to {item (j + 1), item j} -- swap
end if
end tell
end repeat
end repeat
return theList2Sort
end bubbleSortPairedList
I got this bit to work by injecting a custom delimiter (“%%”) around each item. In my case I’m not doing any array mangling, only additions to, but so long as your delimiter is not contained in your data you should be OK.
set the_dom to "fax.spammers"
set the_key to "junk_faxes"
set junk_faxes to read_array(the_dom, the_key)
tell application "Page Sender Fax Center"
try
set this_folder to selection
set this_fax to selection of this_folder as list
if this_fax is {} then
error
end if
on error
display dialog "You must first select a fax in order to add its sender to the spammer list." with icon 2
return
end try
set this_name to fax name of item 1 of this_fax as string
if this_name is not "" then
if this_name is not in junk_faxes then
if the button returned of (display dialog "Add \"" & this_name & "\" to the list of spammers?" with icon 1) is "OK" then
do shell script "defaults write " & the_dom & " " & the_key & " -array-add '%%" & this_name & "%%'"
end if
else
display dialog "Sender \"" & this_name & "\" is already in the list." with icon 2
end if
else
display dialog "Cannot add a blank sender to the list." with icon 0
end if
end tell
on read_array(the_dom, the_key)
set {tid, text item delimiters} to {text item delimiters, "%%"}
set read_plist to text items of (do shell script "defaults read " & the_dom & " " & the_key)
set str2array to {}
repeat with i from 2 to (count read_plist) by 2
set this_item to item i of read_plist
set end of str2array to this_item
end repeat
set text item delimiters to tid
return str2array
end read_array
In case anyone is wondering: This is to add fax senders to a list of spammers. Page Sender (3) can run an AS on receiving a fax and use that to alter the “name” of the fax to indicate a hit. The above is separate and runs manually on a single fax selected in Page Senders window. It does not check for the existence of the delimiter in the data, though that would probably be a good idea…