Writing "data" type values to a pList / Pages Recent Docs

Here’s a defaults/sed method.

set domain to "com.apple.textedit.LSSharedFileList"

-- Parse the "Open Recent" document names from the plist file.
set RecentNames to paragraphs of (do shell script ("defaults read " & domain & " RecentDocuments | sed -En '/^[[:space:]]*CustomListItems[[:space:]]*=[[:space:]]*\\(/,/^[[:space:]]+\\)/ s/^[[:space:]]+Name = \"(.+)\";$/\\1/p '"))

-- Ask which items to remove from the menu.
activate
set namesToCut to (choose from list RecentNames with prompt "Delete document(s):" with multiple selections allowed)
if (namesToCut is false) then error number -128

-- Join the selected names with vertical bars to use as an OR sequence in a regex.
set astid to AppleScript's text item delimiters
-- Escape any regex characters which may occur in the names!
set charactersToEscape to "|()?*+{}{}/."
repeat with i from 1 to (count namesToCut)
	set thisName to item i of namesToCut
	repeat with thisCharacter in charactersToEscape
		if (thisName contains thisCharacter) then
			set AppleScript's text item delimiters to thisCharacter's contents
			set bits to thisName's text items
			set AppleScript's text item delimiters to "\\" & thisCharacter
			set thisName to bits as text
		end if
	end repeat
	set item i of namesToCut to thisName
end repeat
set AppleScript's text item delimiters to "|"
set namesToCut to namesToCut as text
set AppleScript's text item delimiters to astid

-- Derive an edited CustomListItems "array" to use in a "defaults write" shell script.
set newData to quoted form of text 1 thru -2 of (do shell script ("defaults read " & domain & " RecentDocuments |
sed -En '/^[[:space:]]*CustomListItems[[:space:]]*=[[:space:]]*\\(/,/^[[:space:]]+\\)/ {
	H ;
	/^[[:space:]]+\\)/ {
		g ;
		s/[,[:space:]\\n]+\\{[^\"]+\"(" & namesToCut & ")\";[[:space:]\\n]+\\}//g ;
		s/^[^(]*\\(,?/\\(/ ;
		s/(\\n[[:space:]]+\\));/\\1/ ;
		p ;
	} ;
} ;'") without altering line endings)

-- Write the edited array back to the plist file.
do shell script ("defaults write " & domain & " RecentDocuments -dict-add \"CustomListItems\" " & newData)

Edit: Tightened up for potential clashes when there are regex characters in the names.

Hello Nigel! :slight_smile:

Faster and faster!

I really tried to implement something like that, but I wanted to preserve uniqueness of the entries. Say you are removing entries in the plist editor… What I basically wanted to do was to generate a list with delete statements for sed to execute, based on the line numbers, calculated from the item numbers to be deleted.

It turns out that the layout in paragraphs differs for different recent items files. TextWrangler uses 5 lines per file, whereas TextEdit uses 10.

So, I’ll stick with your former solution, that is more than sufficient!

Edit

Getting it on a little distance, I see how I can preserve uniquness among items with the same name.

Calculating the distance in between two items, is what does the trick, and I’m the one having an SDD disk, not
knowing how this performs on a usual HD, I’ll incorporate the changes.

Hello.

If the former was the final version, then this is the optimized version, thanks to Nigels code from above.

Edit

Maybe all of this was in vain, as I use accented characters, and other who use accented characters will discover the same: This version doesn’t treat accented characters very well. you may get \\U008 sequences. in the dialog.

I want to make a point of the fact that the utf-16 is only deleted from the file, not written back, so it looks good under the recent documents of your program.

I am not going to fix this, or try to fix this before I have thought it thru at least. :slight_smile:

Edit++

If the treatment of utf-16 diacriticals doesn’t please you go to the script in post #18 or Shane’s version in post #31.

This won’t be fixed. The reason is that there are two kinds of utf-16 in the Apple-world besides big endian, loose and tight (wrong terms).
The loose encoding scheme stores diacritical’s in separate codepoints, this is what Apple uses for filenames.
With tight encoding, the diacriticals are stored with the character in one codepoint.

Normally, when you use an operating system call to to parse a value, the Darwin layer transforms the loose utf-16 into utf-8 for us. What has happened here, is that the loose utf-16 is stored inside a text file, and doesn’t enjoy the transformation by Darwin.

I am not doing all this for speed, and then having to do a do shell script for each and every name that is going to be displayed"in the hope of managing to decode/encode it right by some construct/incantation, (and lots of trial and errors). It defies the whole purpose of using sed/defaults in the first place.

There are also other possibilities for optimizing this, but the cost/performance ratio expressed by readability/speed, just doesn’t justify it, it is almost undreadable, as it is.

So, stick with it, or let it be. :slight_smile:


-- http://macscripter.net/viewtopic.php?pid=161038#p161038
-- New version that uses sed and defaults for doing the job quicker!
property sTitle : "Recent Items Forensics"
property recentItemIcon : a reference to file ((path to library folder from system domain as text) & "CoreServices:CoreTypes.bundle:Contents:Resources:RecentItemsIcon.icns")
-- We get in a reasonably list of the files with recent items for applications, that we choose one from
set AppleScript's text item delimiters to ""
set plpth to (path to preferences folder from user domain)
set pxpth to POSIX path of plpth
set plists to paragraphs of (do shell script "cd " & quoted form of pxpth & "; ls * |sed -e '/lockfile/ d' -ne 's/\\(.*\\)\\(\\.LSSharedFileList.plist\\)/\\1/p'")
set shtnms to {}
set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "."}
repeat with i from 1 to count plists
	set end of shtnms to (text -2 thru -1 of ("0" & i)) & space & space & (text items -2 thru -1 of item i of plists)
end repeat
set AppleScript's text item delimiters to tids
set chosenapp to choose from list shtnms with title sTitle with prompt "Choose app to clean up recent items for" default items item 1 of shtnms without multiple selections allowed

-- We have chosen an app, we must verify that it isn't running, since it may have unsaved documents, we quit the script if it is so
if chosenapp is not false then
	set chosenapp to chosenapp as text
	set pfnm to text 1 thru 2 of chosenapp
	set chosenapp to text 5 thru -1 of chosenapp
	
	if running of application id (item pfnm of plists) is true then
		display dialog "You must quit  " & chosenapp & " in order to continue." with title sTitle buttons {"Ok"} default button 1 with icon recentItemIcon
		error number -128
	end if
	-- we check to see that the recent item plist isn't a zero sized file on disk!
	tell application "System Events"
		set daPlist to ((item pfnm of plists) & ".LSSharedFileList.plist")
		-- Here we read in the plist file and everything is as earlier.
		if (size of disk item daPlist of (plpth as alias)) = 0 then
			tell me to display dialog "Nothing to remove ..." with title sTitle buttons {"Ok"} default button 1 with icon recentItemIcon
			error number -128
		end if
		
	end tell
	
	set domain to (item pfnm of plists) & ".LSSharedFileList"
	repeat
		set recentNames to paragraphs of (do shell script ("defaults read " & domain & " RecentDocuments | sed -En '/^[[:space:]]*CustomListItems[[:space:]]*=[[:space:]]*\\(/,/\\)/ s/^[[:space:]]+Name = (.+);$/\\1/p '"))
		-- checks to see that the file indeed contains something
		set recentNamesCount to length of recentNames
		if recentNamesCount = 0 then
			display dialog "Nothing to remove ..." with title sTitle buttons {"Ok"} default button 1 with icon recentItemIcon
			error number -128
		end if
		
		
		set namesToCut to {}
		-- We have to create the list, and remove any "" as not every name has gotten those in the recent items list
		repeat with i from 1 to recentNamesCount
			set AppleScript's text item delimiters to "\""
			set recentNmTms to text items of item i of recentNames
			try
				if length of recentNmTms > 1 then
					if item 2 of recentNmTms ≠ "" then
						set item i of recentNames to item 2 of recentNmTms
					else
						set item i of recentNames to item 1 of recentNmTms
					end if
				end if
			end try
			set AppleScript's text item delimiters to tids
			set item i of recentNames to (text -2 thru -1 of ("0" & i)) & space & space & item i of recentNames
			
		end repeat
		
		set namesToCut to (choose from list recentNames with title sTitle with prompt "Delete document(s) from " & chosenapp & "´s Recent Items list:" default items item 1 of recentNames with multiple selections allowed)
		if (namesToCut is false) then error number -128
		
		set numberlist to {} -- We do calculate the ranges, which we are to delete in the current recent items property list file, so we incinerate the items we want to remove
		repeat with i from 1 to (count namesToCut)
			set mn to ((text 1 thru 2 of item i of namesToCut) as number)
			set end of numberlist to "" & (1 + ((mn - 1) * 5)) & "," & (mn * 5) & "d"
			-- *TOTALLY DEPENDENT ON THE LAYOUT OF RECENT ITEMS. 2 LINES UP FRONT, 5 LINES FOR EACH OF THEM.
		end repeat
		-- Join the selected names with vertical bars to use as an OR sequence in a regex.
		set AppleScript's text item delimiters to ";"
		set numberlist to (numberlist as text)
		set AppleScript's text item delimiters to tids
		-- all done with selecting the items, we filter out anything we won't keep
		set newData to quoted form of text 1 thru -2 of (do shell script "defaults read " & domain & " RecentDocuments |sed -e " & quoted form of numberlist without altering line endings)
		-- and writes it back into the existing dictionary
		do shell script ("defaults write " & domain & " RecentDocuments -dict \"CustomListItems\" " & newData)
		
		tell me to display dialog "I removed " & (count namesToCut) & " items(s) from" & chosenapp & "´s Recent items." with title sTitle buttons {"Cancel", "Again"} default button 1 with icon recentItemIcon
		
	end repeat
end if

It’s easier with entry numbers:

set domain to "com.apple.iWork.Pages.LSSharedFileList"

-- Parse the "Open Recent" document names from the plist file.
set RecentNames to paragraphs of (do shell script ("defaults read " & domain & " RecentDocuments | sed -En '/^[[:space:]]*CustomListItems[[:space:]]*=[[:space:]]*\\(/,/^[[:space:]]+\\)/ s/^[[:space:]]+Name[[:space:]]*=[[:space:]\"]*(.*(\\\\\"|[^\"]))\"?;$/\\1/p' |sed  '= ' | sed  'N ; s/\\n/. /'"))

-- Ask which items to remove from the menu.
activate
set namesToCut to (choose from list RecentNames with prompt "Delete document(s):" with multiple selections allowed)
if (namesToCut is false) then error number -128

-- Create individual "delete numbered entry" lines matching the numbers displayed with the selected names.
set cutRegex to linefeed
repeat with i from (count namesToCut) to 1 by -1
	set cutRegex to cutRegex & "s/[,[:space:]]+\\{[^\"}]+(\"[^[:cntrl:]]+\\n[^}]*)?\\}//" & word 1 of item i of namesToCut & " ;" & linefeed
end repeat

-- Derive an edited CustomListItems "array" to use in a "defaults write" shell script.
set newData to quoted form of text 1 thru -2 of (do shell script ("defaults read " & domain & " RecentDocuments | sed -En '
/\\(/,$ H ;
$ {
	g ;" & ¬
	cutRegex & ¬
	"s/^[^(]+\\(,?/\\(/ ;
	s/\\);[^)]+$/\\)/p ;
}'") without altering line endings)

-- Write the edited array back to the plist file.
do shell script ("defaults write " & domain & " RecentDocuments -dict-add \"CustomListItems\" " & newData)

Edit: The regexes in this script no longer assume the file names are in quotes.

Hello.

I’ll leave it as it is, as I can’t easily bypass, or overcome the wrongful encoding of characters with diacriticals in the filenames. :slight_smile:

I have a solid hunch that iconv won’t be able to solve it, since it isn’t a “normal” utf-16 format, and since the text with diacriticals has been transformed into errantly utf-8 before iconv gets it, (After it has been processed by sed.)

If you want to implement your latest version feel free.

I am done with this. (Though a fun pastime during the weekend). :slight_smile:

Hello

The script in post #35 doesn work!.

I was a little bit to quick about it, though I am sure it worked at some point in time, or maybe I didn’t test it fully.

Either way, it doesn’t work, and with my experience of encoding problems, which I cant over come, it won’t be fixed.

The script that works as it should is in post nr #20. Unless you want to roll your own.

To clarify a little: it’s not really a UTF-16 issue, it’s a Unicode issue – UTF-16 is just a way to store Unicode, and the same thing can happen with UTF-8. And the terms you were trying to remember are precomposed and decomposed.

I was dead sure it worked, the code above, the dictionary looks like it should, when it is written out and read back in again, still TextEdit hangs when I go to the recent items menu.

decomposed… That sounds like going out with the garbage, no wonder why I forgot it. :stuck_out_tongue:

I’m sorry for the sloppyness, I know that UTF-X is just different representations of the unicode character set.

What is interesting to know is how you solve this, I have recently got it in the face, when dealing with filenames and the ICU library, I figured I’d either have to compile the byte-sequence back “manually” into precomposed, or find a work around, which I did.

As far as I am concerned it is an Apple issue. :slight_smile:

Are there any routines in any library you know of that performs that service?

Thanks!

See developer.apple.com/library/mac/#qa/qa1235/_index.html

Or to bring it back to AS:

set aFile to POSIX path of (choose file)
tell application id "au.com.myriad-com.ASObjC-Runner" -- ASObjC Runner.app
	set usedFormat to value for key path "decomposedStringWithCanonicalMapping" of item aFile
end tell

That’s one I posted. It does work ” on my machine, at least, and with all the apps and file names I’ve tested.

There’s no script in that post. :rolleyes:

Can I use the code for text and just not files? Because when I deal with filenames, it usually goes good, as they are gotten through some operating system call, (and is how I did it with ICU and wide chars, I just made a representation, of them which couldn’t be translated back, and used the “originals” back and forth to the OS), the trouble arises, when the filenames appears inside a recent items list for instance.

Thank you very much for your sharing!

You know, this is a huge problem in the Linux world, when the guys comes from say Chech republic, and they have accents in their file names. The upside is that they won’t get LInux Thorvaldsto speak about it when they migrate. :smiley:

Yes.

Now that is just sweet!

As for the NSString, I’ll have a look into CFSTring if there is something equivalent, which is better suited for plain C projects.

Thank you very much Shane.

And as far as I know HFS+ was released before Unicode was ready with the complete definition of decomposed characters. So you could be right about decomposed characters in HFS+ doesn’t 100% match with Unicode 3.0 and up. Could be right? Yes, we’re talking about Mac OS (8 and 9) and early Mac OS X (10.0 thru 10.2) versions of the file system.

Since Mac OS 10.2.2 Apple introduced an new file system named HFS+ with journaling. This was for Apple the opportunity to update the file name encoding as well. According to an logical time schedule I think HFS+, since panther, is nowadays Unicode 3.2 and therefore there are no longer issues with decomposed characters.

edit Since I like to keep my posts close to facts I did some searching and found something here that confirmed my assumptions

I never doubted it was following the unicode standard.

By being an Apple issue, I meant that only Apple uses decomposed unicode in the filenames.

I am sure it is for a good reason. And now I know the cure! :slight_smile:

I am so sorry Nigel, and I can’t explain how that happened.

The script that doesn’t work, is my script in post #35, the script that works is in post #18.

Nigel Garvey’s scripts works as far as I know flawlessly.

I’m sorry for the confusion!

My script in post #36 works by deleting numbered entries, so the handling of Unicode characters in the names is something of a cosmetic nicety in its case. However, in getting the edited text into AppleScript with one shell script and then writing the result back to the plist file with another, the number of backslashes in the file tends to increase! This isn’t noticeable in the application itself, which presumably reads the name of the file referenced by the Bookmark data, but the name display in the script on successive runs becomes somewhat grotesque! This version does Unicode conversions and keeps backslashes under control:

pruneOpenRecentMenu("com.apple.iWork.Pages.LSSharedFileList")

on pruneOpenRecentMenu(domain)
	-- Get a list of the application's "Open Recent" menu item names.  
	set RecentNames to getRecentNames(domain)
	
	-- Ask which items to remove from the menu.
	activate
	try
		set namesToCut to (choose from list RecentNames with prompt "Delete document(s):" with multiple selections allowed)
	on error
		display dialog "Either the "Open Recent" menu's empty or the input domain's faulty." buttons {"OK"} default button 1 with icon stop
		set namesToCut to false
	end try
	if (namesToCut is false) then error number -128
	
	-- Create individual "delete numbered entry" lines matching the numbers displayed with the selected names.
	set cutRegex to linefeed
	repeat with i from (count namesToCut) to 1 by -1
		set cutRegex to cutRegex & "s/[,[:space:]]+\\{[^\"}]+(\"[^[:cntrl:]]+\\n[^}]*)?\\}//" & word 1 of item i of namesToCut & " ;" & linefeed
	end repeat
	
	-- Derive an edited CustomListItems "array" to use in a "defaults write" shell script.
	set newData to quoted form of text 1 thru -2 of (do shell script ("defaults read " & domain & " RecentDocuments | sed -En '
/\\(/,$ H ;
$ {
	g ;" & ¬
		cutRegex & ¬
		"s/\\\\\\\\/\\\\/g ; #Adjustment to avoid writing back more backslashes than came out!
		s/^[^(]+\\(,?/\\(/ ;
		s/\\);[^)]+$/\\)/p ;
}'") without altering line endings)
	
	-- Write the edited array text back to the plist file.
	do shell script ("defaults write " & domain & " RecentDocuments -dict-add \"CustomListItems\" " & newData)
end pruneOpenRecentMenu

on getRecentNames(domain)
	-- Parse the "Open Recent" document names from the plist file.
	set RecentNames to (do shell script ("defaults read " & domain & " RecentDocuments |
sed -En '/^[[:space:]]*CustomListItems[[:space:]]*=[[:space:]]*\\(/,/^[[:space:]]+\\)/ s/^[[:space:]]+Name[[:space:]]*=[[:space:]\"]*(.*(\\\\\"|[^\"]))\"?;$/\\1/p' |
sed  '= ' |
sed  'N ;
s/\\n/. / ;
s/\\\\\\\\\"/\"/g ;'"))
	-- Convert any Unicode circumlocutions to Unicode characters.
	set RecentNames to decodeDefaultsUnicode(RecentNames)
	
	return paragraphs of RecentNames
end getRecentNames

on decodeDefaultsUnicode(defaultsText)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "\\\\U"
	set output to defaultsText's text items
	repeat with i from 2 to (count output)
		set thisTextItem to item i of output
		set UniChar to (character id hex2Int(text 1 thru 4 of thisTextItem))
		if ((count thisTextItem) > 4) then
			set item i of output to UniChar & text 5 thru -1 of thisTextItem
		else
			set item i of output to UniChar
		end if
	end repeat
	set AppleScript's text item delimiters to ""
	set output to output as text
	set AppleScript's text item delimiters to astid
	
	return output
end decodeDefaultsUnicode

on hex2Int(hexStr)
	set hexDigits to "0123456789abcdef"
	set int to 0
	repeat with thisDigit in hexStr
		set int to int * 16 + (offset of thisDigit in hexDigits) - 1
	end repeat
	
	return int
end hex2Int

Edit: Empty menu/faulty domain error trap added.

That’s very brilliant Nigel!

Awesome, literally speaking.

I can’t really say what I want to say, because then Administrators will turn up, telling me to mind my language!

I actually wrote an “encoder” but realized that I’d have to do it singlehandedly, since the escaped unicode was spread out into several utf-8 bytes. I took a break, and here it is, with pure AS and sed.
They say seeing is believing, but sometimes one has to wonder.
:smiley:
I have UTF-16 characters with diacriticals that must be precomposed, and Nigels code does the trick!

(For those interested in the decomposed busineess, it turns out that you’ll have to use NSString and not CFString(ref)).

decomposed utf-16?? Tsk. Tsk.You use some Applescript to turn it into precomposed! :slight_smile:

I have taken liberties, I updated the script in post #18 in awe (still) with Nigels script from post #49

Hello.

I have incorporated the error trap into the script in post #18 which should work on any Recent Items List.

Faaast.