Plist files and the "NSRecentDocumentRecords" entries

Hi Folks,

I have Googled & found a few threads associated with using Applescript to inspect plist file entries, but no matter what I try, I can’t quite get the syntax right for making an update, or maybe I am missing something. The article at http://www.mactech.com/articles/mactech/Vol.22/22.03/StoringandAccessingData/index.html was a good start, & goes some way to explaining how database support & plist files are supported.

Some programs set the “NSRecentDocumentRecords” array (as inspected by the Plist Editor PLE) and I would like to delete some entries from this so the recent list avoids overflowing with temporary junk or whatever, keeping files I am more interested in. I understand Applescript now knows about plist files and can update records in them - somehow. Below is the main bit of my simple first attempt to only keep the first 2 items in the list. It fails to update the plist file correctly, even though it looks like it tries to…



tell application "System Events"
	
	-- get the current list from the plist file
	set recent to get (value of property list item "NSRecentDocumentRecords" of contents of property list file listfile)
	
	-- return count value of recent
	-- get |_NSAlias| of |_NSLocator| of item 2 of recent
	
	-- Make a new empty list and then add desired entries to it
	set newlist to {}
	set newlist to newlist & {item 1 of recent}
	set newlist to newlist & {item 2 of recent}
	
	-- replace the old plist file record with the new one
	set value of property list item "NSRecentDocumentRecords" of contents of property list file listfile to newlist
	
	-- check if it worked OK
	get (value of property list item "NSRecentDocumentRecords" of contents of property list file listfile)
	
	return count of newlist


The event log (truncated in places of long data) for this looks like:-


	get value of property list item "NSRecentDocumentRecords" of contents of property list file "/Users/f/Library/Preferences/org.videolan.vlc.plist"
		{{|_NSLocator|:{|_NSAlias|:«data
****0000000001E400020000094D616320484420503100000000000000000000000482B00000024A63B1F6E02000FFFFF0000»}}, {|_NSLocator|:{|_NSAlias|:«data ****00000000018A00020000094D61632048442050310000000000000000000000000000E00012F0000150002000FFFFF0000»}}, {|_NSLocator|:{|_NSAlias|:«data
****00000000014C00020000094D6163204844205031000000000000000000000002F661300012F0000150002000FFFFF0000»}}}

	set value of property list item "NSRecentDocumentRecords" of contents of property list file "/Users/f/Library/Preferences/org.videolan.vlc.plist" to {{|_NSLocator|:{|_NSAlias|:«data
****0000000001E400020000094D61632048442050310000000000000000000000000482B00000024A63B1F6E02000FFFFF0000»}}, {|_NSLocator|:{|_NSAlias|:«data 
****00000000018A00020000094D61632048442050310000000000000000000000000482B0000001533AF1443732E0020FF0000»}}}

	get value of property list item "NSRecentDocumentRecords" of contents of property list file "/Users/f/Library/Preferences/org.videolan.vlc.plist"
		{{|_NSLocator|:{}}, {|_NSLocator|:{}}}


The get shows 3 entries are read
The set shows 2 entries are written AND the parameters look correct
The next get shows 2 empty records - what happened to the data?

Does the applescript syntax for plist files only work when in the nested tell block format?

It is like I am not building my newlist correctly, but the event log looks OK where it is written by the set command…

Many thanks for any help
Cheers
Michael

Model: MacBook Pro 17" Intel
Browser: Safari 533.19.4
Operating System: Mac OS X (10.5)

Hi Folks,
I’m still trying to crack this one (VLC plist file).
What I don’t understand is that the the commands


	set recent to get (value of property list item "NSRecentDocumentRecords" of contents of property list file listfile)

	set end of newlist to item 1 of recent
	set end of newlist to item 2 of recent

	set value of property list item "NSRecentDocumentRecords" of contents of property list file listfile to newlist

looks exactly correct in the event log, and runs with no error messages
vis:-


	set value of property list item "NSRecentDocumentRecords" of contents of property list file "/Users/felicity/Library/Preferences/org.videolan.vlc.plist" to {{|_NSLocator|:{|_NSAlias|:«data ****0000000001A400020000094D616320484420503100000000000C61742E666C7600001300012F0000150002000FFFFF0000»}}, {|_NSLocator|:{|_NSAlias|:«data ****0000000001A400020000094D6163204844205031000000000000xxxF2E666C7600001300012F0000150002000FFFFF0000»}}}

but when I read it back it looks like this in the event log


	get value of property list item "NSRecentDocumentRecords" of contents of property list file "/Users/felicity/Library/Preferences/org.videolan.vlc.plist"
		{{|_NSLocator|:{}}, {|_NSLocator|:{}}}

I have tried varies ways of setting up “newlist”, but at best all I read back is
vis:-


		{{|_NSLocator|:{|_NSAlias|:{}}}, {|_NSLocator|:{|_NSAlias|:{}}}}

It is like I am not building the newlist correctly with the mysterious “data”
How do I dereference or get recognition of the “data” (even though it looks like it is all there correctly when the set writes the plist record)?

Many thanks
Michael

My only suggestion would be… if system events isn’t reading/writing properly then try another tool. There is another tool you can use. Try the command line tool “defaults” for reading/writing the plist. I don’t know if it will make a difference but at least try it.

You can find the defaults tool at /usr/bin/defaults. There’s many examples of how to use it on this website and of course it has a man page.

Good luck!

Hi Regulus6633,

Unfortunatly, the _NSRecentDocumentRecords is a bit of a structure:- a list of records each with a record in them which has the data.
The list exports from defaults as several lines.
The same with PlistBuddy.
I just want to delete some of the records.
Neither defaults or PlistBuddy seem well set up for re-importing such big multi line things.
I don’t think you can pipe into a write action with them, or specify a file.
Nor delete just parts of a plist record.
Perhaps I am wrong and and need to read the man pages better, but it doesn’t stand out.

The other possibility might be to UI script Property List Editor, but that would be totally ugly, fraught, & I guess unreliable. I find it bad enough driving the Airport that way to switch ports.

It seems Applescript was built for this sort of Plist thing.

I thought I just don’t understand referencing and records and types in applescript well enough.
Or perhaps I am missing something from the set value of property list item.
I hadn’t thought this could be a software problem with System Events. Is there are report on this?
Is there a patch or fix to download?

Many thanks for your suggestions and getting back.
I’ll keep looking & trying,…

The copy of VLC I downloaded yesterday uses a separate plist file for its Recent Items and uses different labels for them from yours ” at least in Snow Leopard ” so you’ll have to adapt this yourself and with the caveat that the ‘text’ method may not work in Leopard anyway.

The idea is that the Recent Items are all deleted by setting the ‘value’ of their container to {}. The ones you want to keep are then recreated from the ‘text’ properties of the originals. As I said, this works in Snow Leopard, but its efficacy elsewhere is unknown.

-- Quit VLC if it's running.
tell application "System Events"
	set VLCrunning to (application process "VLC" exists)
	if (VLCrunning) then
		-- There seems to be something wrong with VLC's reaction to 'quit'. Use GUI intead.
		set frontmost of application process "VLC" to true
		keystroke "q" using {command down}
		repeat while (application process "VLC" exists)
			delay 0.2
		end repeat
		-- Give the plist file time to update.
		delay 1
	end if
	
	-- Get the container of the "Recent Items" property list items.
	set plistPath to (path to preferences as Unicode text) & "org.videolan.vlc.LSSharedFileList.plist"
	set CustomListItems to property list item "CustomListItems" of property list item "RecentDocuments" of contents of property list file plistPath
	-- Get the names of the items and their PLIST texts.
	tell CustomListItems's property list items
		set theNames to value of property list item "Name"
		set theTexts to its text
	end tell
end tell

if (theNames is {}) then
	-- Do nothing if VLC has no Recent Items.
	tell application (path to frontmost application as text)
		display dialog "There's nothing in VLC's Recent Items menu." buttons {"OK"} default button 1 with icon caution
	end tell
else
	-- Choose which items to keep.
	tell application (path to frontmost application as text)
		set chosenNames to (choose from list theNames with prompt "What do you want to keep in VLC's Recent Items menu?" with multiple selections allowed and empty selection allowed)
	end tell
	
	if (chosenNames is not false) then
		tell application "System Events"
			-- Delete all the "Recent Items" property list items.
			set value of CustomListItems to {}
			-- Recreate those whose names were chosen, using their PLIST texts.
			repeat with i from 1 to (count theNames)
				if (item i of theNames is in chosenNames) then make new property list item at end of property list items of CustomListItems with properties {text:item i of theTexts}
			end repeat
		end tell
	end if
end if

-- Reopen VLC if it was running.
if (VLCrunning) then tell application "VLC" to activate

I’ve been looking at this today on my OS 10.4 machine, where the Recent Items plist set-up appears to be the same as you describe.

The incomplete ‘value’ records you’re getting appear to be due to the fact that System Events refuses set the value of a property list item to a data object, which you need to do. Most other classes are fine. But with a ‘data’, you get an error if you try to set a property list item’s value to it directly, or, if you set the value to a record containing data, the element which should directly contain the data isn’t created. This happens in both 10.6 and 10.4, so it’s probably the same problem in 10.5.

In 10.4, property list items don’t have a ‘text’ property, so the method I posted above doesn’t work. The script below uses the “defaults” shell command instead to effect the changes, but uses System Events to as an easy way to get the alias data, which the script then converts to POSIX paths to make it easier for the user to select what he/she wants to keep. The script doesn’t work with all applications, but it does with Preview and TextEdit and should do with VLC too, if your plist research is correct.

Edit: I’ve now commented the script and changed a few labels. It also closes and reopens the affected application, if necessary.

(* This script is only known to work in Mac OS 10.4 and (hopefully) 10.5, with applications which reside immediately in the main Applications folder on the startup disk and whose "Save Recent" items are stored in their plist files by the system. *)

-- Reinterpret a list of data values as aliases and return the equivalent POSIX paths.
on dataToPOSIX(theData)
	copy theData to thePaths -- A discrete list isn't actually necessary, but is a precaution against future developments.
	
	-- Reinterpret the data by writing them to a temporary file and reading them back as aliases.
	set fRef to (open for access file ((path to temporary items as Unicode text) & "Alias Data.dat") with write permission)
	repeat with theseData in thePaths
		try
			-- Clear the file.
			set eof fRef to 0
			-- Write the data to it.
			write theseData to fRef
			-- Read back as alias, get the POSIX path, and store that in the list instead of the data.
			set theseData's contents to POSIX path of (read fRef from 1 as alias)
		on error errMsg
			display dialog errMsg buttons {"OK"} default button 1 with icon caution
		end try
	end repeat
	close access fRef
	
	return thePaths
end dataToPOSIX

on quitApp(appName)
	tell application "System Events"
		set wasOpen to (application process appName exists)
		if (wasOpen) then
			-- There's something wrong with VLC's reaction to 'quit' in Snow Leopard. Just in case the same's true on earlier systems, use GUI Scripting instead.
			set frontmost of application process appName to true
			keystroke "q" using {command down} -- Requires GUI Scripting to be enabled.
			repeat while (application process appName exists)
				delay 0.2
			end repeat
			-- Give the plist file time to update.
			delay 1
		end if
	end tell
	
	return wasOpen
end quitApp

-- The main handler. Remove user-chosen entries from an application's "Open Recent" menu.
on deleteRecentItems(appName)
	-- Ensure that the application's not running while the script does its work.
	set wasOpen to quitApp(appName)
	
	tell application "Finder" to get (first application file of folder "Applications" of startup disk whose name is (appName & ".app")) as alias
	set bundleID to bundle identifier of (info for result)
	
	-- Use System Events to get the core data values of the Recent Document entries in the application's Preferences plist file.
	-- They're actually 'alias' data, but are returned here as class 'anything'.
	set plistPath to (path to preferences as Unicode text) & bundleID & ".plist"
	tell application "System Events"
		try
			set anythingData to value of property list item "_NSAlias" of property list item "_NSLocator" of property list items of property list item "NSRecentDocumentRecords" of property list file plistPath
		on error
			set anythingData to {}
		end try
	end tell
	if (anythingData is {}) then
		-- No Recent entries to delete.
		display dialog appName & " has nothing in its Recent Items menu or has not updated its plist file or is not compatible with this script." buttons {"OK"} default button 1 with icon stop
	else
		-- Get POSIX paths from the data and ask the user to choose which ones to keep.
		set thePaths to dataToPOSIX(anythingData)
		set chosenPaths to (choose from list thePaths with prompt "Which Recent Documents do you want to keep in " & appName & "?" with multiple selections allowed and empty selection allowed)
		if (chosenPaths is not false) then -- It wasn't the "Cancel" button that was clicked.
			if (chosenPaths is {}) then
				-- Nothing selected. Prepare a "defaults" string giving an empty Save Recent list.
				set newDefaults to "(" & return & "    {" & return & "    }" & return & ")"
			else
				-- Use "defaults read" to get the existing entries in "defaults" format. Trim the ends of the text to make all the 'text items' below structurally equal.
				set oldDefaults to return & (text from paragraph 2 to paragraph -3 of (do shell script "defaults read " & bundleID & "  NSRecentDocumentRecords")) & (return & "    ")
				-- Edit the text to keep only the entries corresponding to the chosen POSIX paths.
				set astid to AppleScript's text item delimiters
				set AppleScript's text item delimiters to "},"
				set oldDefaults to text items of oldDefaults
				set newDefaults to {}
				repeat with i from 1 to (count thePaths)
					if (item i of thePaths is in chosenPaths) then set end of newDefaults to item i of oldDefaults
				end repeat
				set newDefaults to "(" & newDefaults & "}" & return & ")"
				set AppleScript's text item delimiters to astid
			end if
			
			-- "defaults write" the edited text back to the plist file.
			do shell script "defaults write " & bundleID & " NSRecentDocumentRecords " & quoted form of newDefaults
		end if
	end if
	
	-- If the application was open before, reopen it now.
	if (wasOpen) then tell application appName to activate
end deleteRecentItems

deleteRecentItems("VLC")

Hi Nigel,

Wow, may thanks!!!
You have made some great progress on this and I hope it will be useful to many others.
There wasn’t much helpful when I searched on NSRecentDocumentRecords, and this should help.

Thanks Regulus for first spotting the problem is perhaps with system events on 10.5 (& 10.4)
I expect there is no way of interesting apple in fixing this with 10.5.? when/if it comes out.

I’m running VLC 1.1.7 which is reported as the latest, at least on 10.5.8
As you have probably guessed, I noted this _NSRecentDocumentRecords structure in several plist files, for useful things so I just picked VLC to get started with.

I am surprised that 10.6 uses such a different structure with a different plist file for Recents.
I guess it all must be supported by the tools, else it would have added to application porting maintainance.
I don’t understand why, if it an’t broke…

VLC on 10.5 just has:-
./Library/Preferences/org.videolan.vlc.plist
./Library/Preferences/org.videolan.vlc/vlcrc
./Library/Preferences/VLC/vlcrc

I am new to applescript, and struggle with the syntax, & can see it just needs practice.
It’s not quite as structured as Pascal, eh?
I have spent some time going through your first program for 10.6, and was about to reply.
I had tried varies methods and arrangements used there to see if I could make it work for a data object.
I thought in my ignorance, that I was just missing a particular syntax construct, to educate (trick) applescript into writing the object correctly.

I will study the new 10.4 program and “defaults” calls with particular interest.
I hate to think what “quoted form of newDefaults” looks like, or at least, I would not like to try & prepare it on a shell command line with my usual sed or awk!

You have latched on to a number of things I was thinking, in that applescript provides ways to see the paths and handle them as separate entities. I was going to do my selections by passing that to a shell script, once per, and use success or failure in an applescript if then.

Oh, and I find it is currently crashing on my 10.5.8, eg selecting item 4 …


			repeat with i from 1 to (count thePaths)
				if (item i of thePaths is in chosenPaths) then set end of newDefaults to item i of defaultsData
			end repeat

		"Can't get item 5 of {\"        {
        \\\"_NSLocator\\\" =         {
            \\\"_NSAlias\\\" = <00000000 .......

UNLESS you just select the first item only.
Then I think it re-writes the 10 items anyway?
So I tried deleting the first 3 by selecting the first one anyway and changing (hard coding)


			repeat with i from 4 to (count thePaths)
				if (item i of thePaths is in chosenPaths) then set end of newDefaults to item i of defaultsData
			end repeat

But the write failes

	do shell script "defaults write org.videolan.vlc NSRecentDocumentRecords '(
 }
)'"
		"2011-03-21 22:04:33.690 defaults[94932:613] Could not parse: (
 }
).  Try single-quoting it."

Oh no, a quoting problem…
Was trying to avoid this by avoiding a shell call!

Many thanks for developing such a complete example.
I have just been playing with short snippets to get the understanding.
I thought it would have been simple.

Cheers
Michael

Model: MacBook Pro 17" Intel
Browser: Safari 533.19.4
Operating System: Mac OS X (10.5)

Hi, Michael.

I’ve now made a few edits in the script in post #6 above.

Yes. The version I downloaded is 1.1.7 too. As you say, the storing of the recent item list must be taken care of by the system rather than by the application itself.

On 10.6, I’ve found:
./Library/Preferences/org.videolan.vlc.plist
./Library/Preferences/org.videolan.vlc.LSSharedFileList.plist
./Library/Application Support/org.videolan.vlc/ml.xspf

You haven’t shown the entire error message, but my best guess is that my choice of text item delimiter doesn’t work with the “defaults read” result on your system. I’ve now changed it to a shorter delimiter which should work on both systems, but if it doesn’t, could you post a complete typical result with two or three “recent items” in it?

Hi Nigel,

Many thanks for a great solution, works like a dream.
The comments help too.

I suppose I was originally hoping that class “anything” being used for “alias” data could be written to a plist file by some syntax I didn’t know about.

This is a great instruction on how to quote and send a big structure into the “defaults” program.
Maybe one could backwards quote to achieve the same sort of thing in a shell script.

I see the UI scripting tries quitting in a repeat loop several times.
Hmmmmm, I guess repeated attempts is standard practice to improve reliability in UI scripts.
That might explain some problems I have with other UI scripts being unreliable when they run at night, but work fine every time I try to work out why they fail.

Cheers
Michael

Model: MacBook Pro 17" Intel
Browser: Safari 533.19.4
Operating System: Mac OS X (10.5)

Hi, Michael.

Thanks for the feedback. I’m glad the script works on your system.

You had me worried for a moment. :wink: There’s only one attempt to quit the application, but there’s a delay repeat within the process which makes sure that the app’s completely gone and the plist file updated before the script continues.
¨

GUI Scripting’s best used as a last resort, when there’s no other way to achieve what you want. It’s very vulnerable to timing errors and differences between systems such as structural differences between versions, different user languages, and different user preferences.

The timing difficulties arise from the fact that you’re controlling the app by proxy. When an app’s scripted directly, the script waits for it to signal that each command’s been completed before issuing the next command. (Sometimes, of course, an application may instead return an acknowledgement when a process has been started, so that the script can control what happens during that process.) With GUI Scripting, the only acknowledgement the script receives is that System Events has carried out a given action on the application’s GUI interface. The only way to know how the application’s responding to that action is to test repeatedly for some expected change until it happens. In the case of quitting an app though, I imagine that, even when scripted directly, the application doesn’t send an acknowledgement that it’s completely dead, so a test repeat would be needed here anyway.