Store, Retrieve, Update Property list file without overwriting it

I am working on a script to group file names and store them in a property list (.plist) file.
The following function takes in a collective name for the group of files (ie. “Group1”) and a list of files belonging to it (ie. {“File1.png”, “File2.png”, “File3.png”}). It is supposed to write the information to the plist in the following format:

lastUpdate 2018-06-28T05:07:43Z groups Group1 File 1.png File 2.png File 3.png Group2 File 4.png File 5.png File 6.png

on saveToPlist(groupName, groupFiles)
	
	tell application "System Events"
		set theParentDictionary to make new property list item with properties {kind:record}
		set thePropertyListFilePath to "~/Desktop/Groups.plist"
		
		-- Get / Create Property List File
		if not (exists file thePropertyListFilePath) then
			set thePropertyListFile to make new property list file with properties {contents:theParentDictionary, name:thePropertyListFilePath}
			log "New plist created"
		else
			set thePropertyListFile to property list file thePropertyListFilePath
			log "Plist exists"
		end if		
		
		-- Add to Property List File
		tell property list items of thePropertyListFile
			make new property list item at end with properties {kind:date, name:"lastUpdate", value:current date}
			
			if not (exists property list item "groups") then
				set groupsRec to make new property list item at end with properties {kind:record, name:"groups"}
				log "groups record created"
			else
				set groupsRec to property list item "groups"
				log "groups record already exists"
			end if
			
			make new property list item at end of groupsRec with properties {kind:list, name:groupName, value:groupFiles}
		end tell

	end tell
end saveToPlist

  1. There is a problem with the last section (adding to the plist file). The script doesn’t seem to find the property list item “groups”. I would also like to like the script to append to the “groups” record instead of replacing the content completely.

  2. How would I access the values in each group?

  3. How would I add/remove individual values in each group’s array once they are already in the file?

Any help is appreciated. Please let me know if you need clarification on anything. Cheers.

Dealing with anything but the simplest property list file in System Events is slow and painful. It’s much simpler and faster to use AppleScriptObjC. That way you can just copy-and-paste the handlers to read and write the file, and manipulate the information as a normal AppleScript record.

Here’s an example:

use AppleScript version "2.5" -- 10.11 or later
use framework "Foundation"
use scripting additions

set thePath to POSIX path of ((path to desktop as string) & "Groups.plist")
-- define your info as a record or array
set theInfo to {lastUpdate:(current date), groups:{Group1:{"File1.png", "File2.png", "File3.png"}, Group2:{"File4.png", "File5.png", "File6.png"}}}
-- store it in a file
my writeToPlist:theInfo inFile:thePath
-- read from file
set newInfo to my readPlistFrom:thePath
-- modify info
set groups of newInfo to groups of newInfo & {Group3:{"File7.png", "File8.png", "File9.png"}}
set lastUpdate of newInfo to (current date)
--write newInfo to property list
my writeToPlist:newInfo inFile:thePath

on writeToPlist:someASThing inFile:posixPath
	-- convert to property list data
	set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:someASThing |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference)
	if theData is missing value then error (theError's localizedDescription() as text)
	-- write data to file
	set theResult to theData's writeToFile:posixPath atomically:true
	return theResult as boolean -- returns false if save failed
end writeToPlist:inFile:

on readPlistFrom:posixPath
	-- read file as data
	set theData to current application's NSData's dataWithContentsOfFile:posixPath
	-- convert to Cocoa object
	set {theThing, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 |format|:(missing value) |error|:(reference)
	if theThing is missing value then error (theError's localizedDescription() as text)
	-- we don't know the class of theThing for coercion, so...
	set listOfThing to current application's NSArray's arrayWithObject:theThing
	return item 1 of (listOfThing as list)
end readPlistFrom:

2 Likes

Thank you for the hint with AppleScriptObjC. I see what you are doing with theInfo at the very top, in my quest to make it as dynamic as possible, I have also included the following code to assign a name to the group (instead of calling it Group1…3 etc), and pass in a list.


-- A list
set theList to {"File1", "File2", "File3"}
-- Assign Name to Group
set groupName to the text returned of (display dialog "Name of this group:" default answer "")

I then update using:


...
set newInfo to my readPlistFrom:thePath
set groups of newInfo to groups of newInfo & {groupName:theList} --> Issue described below
my writeToPlist:newInfo inFile:thePath
...

How do I set the name of the group to my input dynamically (ie. the string in groupName - returned from the dialog)? Right now, it saves it as “groupName” instead of the string contained in the variable. (theList saves just fine)

ASObjC can handle that simply too:

set theRecord to (current application's NSDictionary's dictionaryWithObject:"File10.png" forKey:groupName) as record

Shane, it now successfully saved the record using the dialog result as key.

When I read back the file using the readPlistFrom routine, I am unable to enumerate the results in a manner that allows me to tell the routine the key and get back a list of values for that key, rather than a dump of everything.

on getValuesForKey:theKey
	--> return values at theKey as list
end getValuesForKey:

I would also like to be able to delete a key (and thus any values associated with it) by specifying which key to remove.

on deleteRecord:theKey
	--> delete record (key-value(s) pair) at theKey
end deleteRecord:

Lastly but not least, I would like to manipulate a specific value under a specific key, in order to update the record for that key only.

on manipulate:theKey atValue:theValue newValue:newValue
	--> update/replace theValue under key theKey with newValue
end manipulate:atValue:newValue:

From a few hours of research, I know about removeObjectForKey but can’t seem to implement it correctly. Does the file need to be read in, key-values removed and saved or is there a simpler way that does it all in one sweep.

Appreciate the help with the 3 routines above.

This is also probably better done in ASObjC. If you’re comfortable with it, you could modify the second handler so it just returns theThing, which in this case with be an NSDictionary. You can also change the options parameter to (current application’s NSPropertyListMutableContainersAndLeaves) so you can modify it in place.

Anyhow, once you have the groups dictionary, you can do something like this rough outline:

set theKeys to groupsDict's allKeys()
repeat with oneKey in theKeys
 -- retrieve value
 set itsValue to groupDict's objectForKey:oneKey
  -- or to change it
 if oneKey as text = "Group1" then
  groupDict's setObject:newListOfEntries forKey:oneKey
  -- or to delete it
 if oneKey as text = "GroupUnwanted" then
  groupDict's setObject:(missing value) forKey:oneKey

That should get you started.

This helped me a lot today. However, I think now AppleScriptObjC doesn’t care much for “string” in line 5. It seems to prefer “text”. Thanks.

An example of a hybrid script for writing/reading AppleScript records from me:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

-- write
do shell script "defaults write my.domain myRecord -dict 'aString' -string 'Hello,word!' 'anInteger' -integer '1' 'aReal' -float '3.14'"
delay 1

-- read back
set plistPath to POSIX path of (path to library folder from user domain) & "Preferences/my.domain.plist"
set plistData to my (NSData's dataWithContentsOfFile:plistPath)
set plist to (my (NSPropertyListSerialization's propertyListWithData:plistData options:0 format:(my NSPropertyListXMLFormat_v1_0) |error|:(missing value)))
set theRecord to (plist's objectForKey:"myRecord") as record

--> {aString:"Hello,word!", aReal:3.140000104904, anInteger:1}