Manipulating Plists

From Provider Wiki

Jump to: navigation, search

There are a number of times when a system administrator in charge of a group of Macintosh computers might need to change the settings on a computer programatically. Chief among these would be when he or she wants to make the same changes on a large group of computers.

When trying to make changes like this there are a few approaches:

  1. You can manually make the changes on a computer, or use AppleScript and GUI Scripting. This method requires you to turn on GUI scripting on the computer (manually), and is very prone to errors and completely breaks when Apple changes something in a future update.
  2. The second method is to setup the preferences on one machine, and then copy the preferences to the other computers. In extreme cases this can even done by creating an image of the computer as you want it, stripping out some specific things, then pushing that image on multiple computers. There are a number of variants of this technique, and they are very appropriate in certain circumstances.
  3. For many common settings changes that you might want to enforce Apple has their Workgroup Management system as part of MacOS X Server (we have a article on how to use it without MacOS X Server as well). This is a great solution for those things it works for, and can even enforce those settings in a way that no other method can, but it is limited in many ways.
  4. The final method, and the one the rest of the article will cover is to manipulate the Plist files directly (Plist being the file format that Apple uses for preference files). We will be covering how to do this through a variety of methods.

Contents

About Plists

'plist' is the preference format that Apple inherited from the NeXT OS that it bought for inclusion in MacOS X. It is a hierarchical format where every node is one of three types of objects:

  1. Array: an ordered list of objects
  2. Dictionary: a list of objects where each object has a keyword associated with it
  3. String: a series of characters
  4. Number: either a decimal or a integer number
  5. Boolean: either "Yes" or "No" (internally represented as 1 or 0)
  6. Date: a date and time stored as a string (such as "2007-07-19T14:12:49Z")
  7. Data: binary data, usually Base64 encoded

The first two types of objects can contain any mixture of the objects (so a dictionary can contain another dictionary, and an array can contain a dictionary next to a string).

If you have Apple's developer tools installed (free download with registration at [1]), then you should have "Property List Editor.app" on your computer (/Developer/Applications/Utilities/). This is a great tool for viewing plists, and can help explore and manipulate them.

Plists can be represented in XML, but they are a slightly more limited form of XML. Tags cannot have attributes. This might seem a bit limiting, but this is the latest expression of a system that predates XML.

Plists currently come in three flavors: old-style, xml, and binary. The three are completely interchangeable, and you will not see the difference with most of the tools we will cover. The old-style is depreciated and you will not find it in a file, but many of the tools still display it and read it. The big difference is that the binary version is a bit faster to read, and if you try and read the later with a text editor you will not get a good result.

An important thing to note is that many applications only read their preferences when they startup, and they re-write them as they quit, ignoring changes that might have been made. This means that you probably want to make sure that the application that you want to modify should not be running when you make changes.

Using 'defaults'

The simplest method to use is the command line program 'defaults'. It is included on every MacOS X install. It is very easy to include in shell scripts, or to blast out to a number of computers using Apple Remote Desktop's "do shell script" command. However, there are a number of limitation to the 'defaults' that make it only suitable for the most trivial of changes.

'defaults' is always called with 'defaults' plus a verb after it (read, write, delete, and others), followed by the "domain" you are looking for (usually an application), then an optional path, followed by a value where appropriate. We will give examples of the "read" and "write" verbs. The domain can be one of the folowing:

  1. -a and and application name
  2. the reverse-dns name for an application, such as com.apple.Mail (not case-sensitive)
  3. a path to a preference file that you have access to, minus the ".plist" at the end
  4. or -globalDomain which gets everthing

Also note that you can use -currentHost to get things that belong to this computer only (this is useful for things like 802.1X settings).

To get more comprehensive descriptions of what is available please see 'defaults --help' or 'man defaults'.

defaults read

Sometimes you just need to read out a value for a single setting, or comb through all of the setting on a computer. And here is where you the 'read' command available through 'defaults' comes in handy. To see a list of all of the preferences that are stored for the user you are logged in as at the moment simply type this at a terminal prompt:
defaults read
The results will be a very long list of every setting that can be found in ~/Library/Preferences A bit more manageable demonstration would be to provide a "domain". As an exmaple:
defaults read com.apple.Mail

Also note that you can always get the xml version by using the '-xml' flag in the command.

defaults write

Note: you can really mess things up with this, so do be careful with this.

This version is obviously how you write to plists to change them. For this verb you have to provide just about everything. For these examples we will be writing a new file for a fictional appliation "PlistTest".

First off we will create a setting for whether the application should crash or not. So we will write this as:
defaults write edu.upenn.PlistTest doICrash No
Note that the file ~/Library/Preferences/edu.upenn.PlistTest.plist was created in this last command without us being asked, and now contains something like this (note that it might be the binary version, so use Property List Editor.app:
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>doICrash</key> <string>No</string> </dict>

</plist>
Or through 'defaults read' you will get:
{doICrash = No; }
Note that it is being represented as a "String" type. 'defaults' has guessed at what I want, but not correctly in this case, so I have to give it a hint about what I want. So here is the command I really wanted:
defaults write edu.upenn.PlistTest doICrash -bool No
Here is a bit more complicated example: Say you wanted to add a string to an array inside a dictionary. By using the "old format" to give in to defaults you can accomplish this:
defaults write edu.upenn.PlistTest '{accounts = ({theItem = theValue; }); }'
The easiest way of generating this is to create what you want in Property List Editor.app, then read out the value with 'defaults read'. This does have two major problems:
  1. The format is really difficult to follow for complex things.
  2. Along the path you follow you are going to be re-writing everything. For example, if there were already other items in "accounts" they would be wiped out. This is the major problem with the 'defaults write' approach.

You can use the -array-add and -dict-add verbs to try and get around this, but it often just not possible to use those to accomplish what you want, especially if you have nested dictionaries and arrays.

Using PlistBuddy

PlistBuddy is the tool that Apple has come up with to help them manipulate plists for their installers. Unfortunately, thus far Apple has not promoted PlistBuddy to be a supported tool, so you have to be a little creative to use it. There is some question about whether you can distribute it, so the best way is to find a copy that one of Apple's installers has left behind on your computer.

Locating a copy of PlistBuddy

Since Apple's installers and updaters almost all use PlistBuddy, there is a copy of it on almost every MacOS X computer out there. To get a list of them use this command:
find /Library/Receipts -name *PlistBuddy
It is a good idea to use the latest version of PlistBuddy that you can. Here is a quick perl script that can get this for you:
#!/usr/bin/perl

@FileLocations = split("\n", `find /Library/Receipts -name *PlistBuddy 2>/dev/null`);

if (scalar @FileLocations == 0) {
        die "There was no copy of PlistBuddy found on this computer!";
}

$currentBestTimeStamp = 0;
$currentBestLocation = "";

foreach $thisFile (@FileLocations) {
        $theTimeStamp = (stat($thisFile))[9];
        if ($theTimeStamp > $currentBestTimeStamp) {
                $currentBestTimeStamp = $theTimeStamp;
                $currentBestLocation = $thisFile;
        }
}
print $currentBestTimeStamp . " " . $currentBestLocation;

The command line PlistBuddy

PlistBuddy nicely includes some help information to get you started (just run it with the "--help" option), but for a even quicker start, here are a few examples:

To read out the hostname of the second mail account in the current user's Mail.app setup just run the following comand:
./PlistBuddy -c "Print :MailAccounts:1:Hostname" ~/Library/Preferences/com.apple.mail.plist
Note: this assumes that you have "cd"ed to the directory containing PlistBuddy before executing the command first. Just doing this would be difficult with the defaults command. But PlistBuddy really shines when you want to make complex changes to a plist. As an example if you wanted to add the PoBox SMTP server to Mail.app. Here is a quick perl script that would do that (assuming you filled in the $plistBuddyLocation and $userName variables):
#!/usr/bin/perl

$plistBuddyLocation = "enter the path to PlistBuddy here";
$userName = "enter the user name here";

$uniqueId = `uuidgen`;
$targetFile = "~/Library/Preferences/com.apple.mail.plist";

`$plistBuddyLocation -c 'Add :DeliveryAccounts:0 dict' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:AccountType string "SMTPAccount"' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:Hostname string "pobox.upenn.edu"' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:SSLEnabled string "YES"' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:ShouldUseAuthentication string "YES"' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:Username string "$userName"' $targetFile`;
`$plistBuddyLocation -c 'Add :DeliveryAccounts:0:uniqueId string "$uniqueId"' $targetFile`;

Note: this example simply adds another entry without checking to see if one is already there and does not set any of the accounts to use this new entry. And it might make more sense to do the "YES"s as booleans, Mail.app actually looks for them to be strings.

Using Perl and the Perl-ObjC bridge

O'Reilly has a nice intro do using Perl to manage plists that can be found here: [2]. If you are going to be using this much I would encourage reading that article fully. But as a taster of what you can do:

First off: an overview. Perl does not natively work with Plists. There are a few modules in [CPAN] that are nice starts on modules to handle this sort of a job, but none of them are really complete, and most importantly: none of them are installed by default. However, Apple has provided a bridge between Obj-C and Perl, so you can call methods from the former in the latter (or the reverse). So with a little work we can use the Objective-C methods that Apple provides to manipulate Plist objects in Perl. It is helpful to have an understanding of Objective-C, but it is not strictly required to go this route.

Rather than write about what you need to do, here is an annotated script that should get you going:

Annotated sample script

#!/usr/bin/perl

use Foundation;
This call tells Perl that we want to use the "Foundation" set of calls, which contains the most basic calls in Apple's Obj-C toolbox. This contains everything we need to manipulate plists.
$unexpandedFilePath = NSString->stringWithString_("~/Library/Preferences/com.apple.mail.plist");
$filePath = $unexpandedFilePath->stringByExpandingTildeInPath();
if (! $filePath or ! $$filePath) {
        die "Unable to get expanded file path!";
}

A few things to note here: We are using the NSString library out of Obj-C to turn the tilde-path into a full path. This is very useful when you are running as root. The second thing is that when there is input to pass to a function we have to end the function name with an underscore. The final thing to notice is the last three lines. There we are checking that stringByExpandingTildeInPath did not return NULL. But in Perl this is seen as an object that is NULL, so we have to check both $ and $$. This is a really good habit to get into, and similar lines will be sprinkled throughout this example.

$preferences = NSMutableDictionary->dictionaryWithContentsOfFile_($filePath);
if (! $preferences or ! $$preferences) {
        die "Unable to get the plist!";
}

$deliveryAccounts = $preferences->objectForKey_(DeliveryAccounts);
if (! $deliveryAccounts or ! $$deliveryAccounts) {
        die "Unable to get the accounts array!";
}

Here we are getting an object that is the contents of the file. By definition all Plists start out with a dictionary as the top level, so we can take advantage of NSDictionary's convenience method to get things setup. Then we are getting the array of delivery accounts. We are using the NSMutableDictionary form to so that we can modify the objects.

$accountsCount = $deliveryAccounts->count();
for ($i = 0; $i < $accountsCount; $i++) {
        $thisAccount = $deliveryAccounts->objectAtIndex_($i);

        if(lc($thisAccount->objectForKey_("Hostname")->UTF8String()) eq "pobox.upenn.edu") {
                $thisAccount->setObject_forKey_("smtp.pobox.upenn.edu", "Hostname");
        }
}

Here we have most of the work we are going to do in this script: changing any smtp hostnames from "pobox.upenn.edu" to "smpt.pobox.upenn.edu". We loop through the accounts and replace any of them that match. Note that we have not actually made any changes on disk at this point. Another important point is that to use the string in Perl it needs to be converted to a UTF string. This is because NSString can handle a lot more types of strings than Perl can, so we have to shoe-horn it into a specific type.

$preferences->writeToFile_atomically_($filePath, "YES");

And with that, we have made our changes.

Using Python

Python has the cleanest interface to plists, but suffers from a few glaring problems: as installed on MacOS X (as of 10.4.10) it will not work with Plists with dates in them, and it will not work with binary plists.

In order to support dates it needs to have the PyXML packing installed, and this is not installed on MacOS X (as of 10.4.10). If you do not have this package installed and try to use a plist that contains a single date field you will get an error and be unable to use the plist. This is probably the biggest hudle for using Python to process plists. If you run into this problem you will probably get an "exceptions.ImportError" exception. Possible work-arounds would include removing the date data before you use python or carrying the PyXML package with you (putting it in a package with your scripts).

The binary plist problem is much easier to deal with. If you are in this case you probably will get an "xml.parsers.expat.ExpatError" exception. The easy solution to this is to force the plist into xml format with the "/usr/bin/plutil" tool (this should be installed on all computers). It should be used like this:
os.system("/usr/bin/plutil -convert xml1 %s" % 'fileName' )
There should be little difference for most applications between a xml and binary plist, but you may want to re-convert the plist back to binary (binary1) once you have written it back out:
os.system("/usr/bin/plutil -convert binary1 %s" % 'fileName' )


Un-annotated sample script

This script attempts to set the users AddressBook to use Penn's online directory. It is not a complete script in that it does not set the password needed to access this, and it blindly assumes that the current short username is the proper user name to use.

#!/usr/bin/python

import os.path
import plistlib
import sys
import xml.parsers.expat as expat
import commands

wasBinary = False
userID = commands.getoutput("/usr/bin/whoami")

expandedPath = os.path.expanduser('~/Library/Preferences/com.apple.Addressbook.plist')
try:
	preferences = plistlib.Plist.fromFile(expandedPath)

except (expat.ExpatError):
	wasBinary = True
	os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath )

	try:
		preferences = plistlib.Plist.fromFile(expandedPath)

	except (ImportError):
		os.system("/usr/bin/plutil -convert binary1 %s" % expandedPath )
		print "The plist at '%s' has a date in it, and therefore is not useable.\n" % expandedPath
		sys.exit()

except (ImportError):
	print "The plist at '%s' has a date in it, and therefore is not useable.\n" % expandedPath

except:
	print 'Unexpected error:', sys.exc_info()[0]
	sys.exit()

# Here we should have a useable preferences file

if not(preferences.has_key("AB3LDAPServers")):
	preferences["AB3LDAPServers"] = []

theEntry = None
for server in preferences.AB3LDAPServers:
	if server.ServerInfo.HostName.lower() == "directory.upenn.edu":
		theEntry = server

if not(theEntry):
	# here we need to add a entry
	theEntry = {}
	theEntry["ServerInfo"] = {}
	theEntry["ServerType"] = 0
	preferences["AB3LDAPServers"].append(theEntry)

# Now that we have something to start with, lets set everything
theEntry["ServerInfo"]["AuthenticationType"] = True
theEntry["ServerInfo"]["Base"] = "ou=PennPeople, dc=upenn, dc=edu"
theEntry["ServerInfo"]["Enabled"] = True
theEntry["ServerInfo"]["HostName"] = "directory.upenn.edu"
theEntry["ServerInfo"]["Port"] = 389
theEntry["ServerInfo"]["Scope"] = 2
theEntry["ServerInfo"]["SSL"] = True
theEntry["ServerInfo"]["Title"] = "Penn Online Directory"
theEntry["ServerInfo"]["Username"] = "uid=%s, ou=PennPeople, dc=upenn, dc=edu" % userID

if not(theEntry["ServerInfo"].has_key("UID")):
	theEntry["ServerInfo"]["UID"] = commands.getoutput("/usr/bin/uuidgen")
	
# Now to write things back to the file
try:
	preferences.write(expandedPath)
	# preferences.write("/tmp/com.apple.Addressbook.plist")
except:
	print "Unable to write to file: %s" % expandedPath
	
# And finally we should leave the plist in the form we found it
if wasBinary:
	os.system("/usr/bin/plutil -convert binary1 %s" % expandedPath )

Please note that this assumes that the users PennKey is the same as their current user name.

Personal tools