Writing an array of strings to a file

A question in a recent thread (here) was how one might save an array of strings to a file, and there are three alternatives that I am aware of.

The approach I have always used is NSArray’s writeToFile method:

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (path to desktop) & "Test File.txt"
set theArray to current application's NSArray's arrayWithArray:{"a", "b", "c"}
theArray's writeToFile:theFile atomically:true -- write file
set theArray to current application's NSArray's arrayWithContentsOfFile:theFile -- read file

The following is a suggestion by Fredrik71; it uses the NSKeyedArchiver class. For consistency, I modified the script to use file rather than URL.

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (path to desktop) & "saveData.dat"
set theArray to current application's NSArray's arrayWithArray:{"a", "b", "c"}
set theData to current application's NSKeyedArchiver's archivedDataWithRootObject:theArray
theData's writeToFile:theFile atomically:true -- write file
set theData to current application's NSData's dataWithContentsOfFile:theFile
set theArray to current application's NSKeyedUnarchiver's unarchiveObjectWithData:theData -- read file

The third approach is loosely derived from posts by Shane and uses the NSPropertyListSerialization class:

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (path to desktop) & "Test File.plist"
set theArray to current application's NSArray's arrayWithArray:{"a", "b", "c"}
set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:theArray format:(current application's NSPropertyListBinaryFormat_v1_0) options:0 |error|:(reference)
set theResult to theData's writeToFile:theFile atomically:true -- write to file
set theData to current application's NSData's dataWithContentsOfFile:theFile
set theArray to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 format:(missing value) |error|:(missing value) -- read file

I wondered if there are any compelling reasons to use one approach over the other? A few miscellaneous thoughts:

  • NSArray’s writeToFile method is deprecated, and Shane has commented that “direct saving of arrays is discouraged these days”.
  • The NSKeyedArchiver class will save additional types and, apparently, will save multiple arrays (or other objects) identified by keys.
  • The archivedDataWithRootObject method is deprecated, although there is a simple alternative.
  • All three approaches took about 1.7 millisecond to run.

Approach 1 and 3 do exactly the same, they serialize the array to Property List. The writeToFile method is deprecated because it doesn’t provide any error handling unlike NSPropertyListSerialization.

archivedDataWithRootObject in approach 2 is deprecated, too. The more secure replacement is archivedDataWithRootObject:requiringSecureCoding:error:. But NSCoding for a simple string array is pretty overkill.

A lightweight fourth approach is serializing to JSON (with NSJSONSerialization).

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (path to desktop) & "Test File.json"
try
	set {theData, theError} to current application's NSJSONSerialization's dataWithJSONObject:{"a", "b", "c", "d"} options:0 |error|:(reference)
	if theError is not missing value then error (theError's localizedDescription()) as text
	set theResult to theData's writeToFile:theFile atomically:true -- write to file
	set theData to current application's NSData's dataWithContentsOfFile:theFile
	set {theArray, readError} to current application's NSJSONSerialization's JSONObjectWithData:theData options:0 |error|:(reference) -- read file
	if readError is not missing value then error (readError's localizedDescription()) as text
	set theArray to theArray as list
on error e
	display dialog e
end try

It avoids the overhead of the Property List XML.

Side-note: It’s not necessary to create a Foundation array from an AppleScript array. The framework can infer it. And please handle all errors

My favorite approach is 4 followed by 3

1 Like

Stefan. Thanks for the great information. It answers my questions and provides a new approach that works well.

BTW, I wrote the scripts in post 1 for discussion and testing purposes only, and, for this reason, I disagree that they need error handling.

I was curious as to the sizes of the files created by the four (now five) approaches, and I tested them with an array that contained 5,286 items.

Approach 1 (NSArray) - 127 KB
Approach 2 (NSKeyedArchiver) - 111 KB
Approach 3 (NSPropertyListSerialization binary) - 53 KB
Approach 4 (NSJSONSerialization) - 42 KB
Approach 5 (NSPropertyListSerialization XML) - 127 KB

The timing results with the same array are shown below. These results include the time it took to read a text file and make it into an array, which timed separately took 2.3 milliseconds.

Approach 1 (NSArray) - 8.8 milliseconds
Approach 2 (NSKeyedArchiver) - 9.0 milliseconds
Approach 3 (NSPropertyListSerialization - binary) - 5.5 milliseconds
Approach 4 (NSJSONSerialization) - 4.1 milliseconds
Approach 5 (NSPropertyListSerialization XML) - 9.0 milliseconds

The reason for the different outcome of NSArray and NSPropertyListSerialization is that NSArray writes always plain XML while you explicitly specified the binary format in the NSPropertyListSerialization approach.

You should get the same file size and (almost the same) speed with the format NSPropertyListXMLFormat_v1_0

Of course plain XML is the most expensive format with regard to file size because of the header and the XML tags

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<string>a</string>
	<string>b</string>
	<string>c</string>
</array>
</plist>

On the other hand JSON is the most efficient format because it contains just the essential data

["a","b","c"]

Thanks Stefan. I edited script 3 to use the XML property list format. I retested and added approach 5 to my file-size and timing-results above. The results were as expected.

FWIW, I looked at the documentation for the NSJSONSerialization class, and the same approach can be used for a dictionary. The following is a simple example for demonstration purposes only:

use framework "Foundation"
use scripting additions

set theFile to POSIX path of (path to desktop) & "Test File.json"
set listOne to {"a", "b"} -- can also be an array
set listTwo to {"c", "d"} -- can also be an array
set theRecord to {keyOne:listOne, keyTwo:listTwo} -- can also be a dictionary

-- write file
set theData to current application's NSJSONSerialization's dataWithJSONObject:theRecord options:0 |error|:(missing value)
theData's writeToFile:theFile atomically:true

-- read file
set theData to current application's NSData's dataWithContentsOfFile:theFile
set theDictionary to current application's NSJSONSerialization's JSONObjectWithData:theData options:0 |error|:(missing value)

-- test
((theDictionary's valueForKey:"keyTwo")'s objectAtIndex:0) as text --> "c"

JSON supports the following types

  • the collection types Array and Dictionary (Dictionary keys are required to be String)
  • the value types
    • String – everything in double quotes
    • Boolean – true or false not in double quotes
    • Integer – numeric values without fractional digits
    • Floating point – all other numeric values
    • Null – the AppleScript missing value