MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Using Perl to Manage Plist Files

by James Reynolds
07/29/2005

A year and a half ago, I wrote an article on scripting Mac OS X. One of the most common feedback questions was how to manage complex Plist files with scripts. The defaults command, which is often used to manage simple values in Plist files, does not easily manage the nested arrays or dictionaries that are present in most Plist files.

Recently, I learned about the PerlObjCBridge that was included in Mac OS X 10.3. It was one of those new OS features that got so little attention, that it wasn't until Mac OS X 10.4 came out that I became aware of it. It allows you to use Cocoa code in Perl scripts. Cocoa can easily manage Plist files. When I first heard about the PerlObjCBridge, I was ecstatic; specifically, because I can now manage Plist files with my scripts.

As I learned how to use the PerlObjCBridge, I also learned about Camelbones. Camelbones is another Perl-to-Cocoa bridge. PerlObjCBridge and Camelbones are very similar; not just in what they do, but in usage and syntax as well. The PerlObjCBridge ships with Mac OS X 10.3 and above. However, it can not be used to create a GUI application. You can create GUI applications with Camelbones. I will focus on the PerlObjCBridge because it is included with the OS so you do not need to install anything, and because we do not need a GUI to manage Plist files.

This topic will be covered by two articles. In the first, I will compare a Cocoa example with a Perl version and show a few differences between Cocoa and Perl notation. Then I'll provide a Cocoa tutorial, including how to create objects and a little about pointers and references. Finally, I'll jump into Plist file management by loading the computer network preferences from a Plist file and reading values, even nested values.

Related Reading

Learning Perl
By Randal L. Schwartz, Tom Phoenix, brian d foy

The second article will start by showing how to change values, such as the computer's name, and how to save the changes back to disk. Then it will cover scanning a Plist file. For an example, I'll get a dump of the NetInfo database and look at all the users in the database. In essence, this example will show how to convert Cocoa arrays and dictionaries to Perl arrays and hashes. The second article will close by creating a Plist file from scratch. For an example, I'll create a Plist file of an Xgrid batch submission. The core of this last example will show how to convert from Perl arrays and hashes to Cocoa arrays and dictionaries.

In writing these articles, I assume you know a little Perl, so I wont explain things like variables, loops, how to create and execute scripts, or most scripting jargon. If you need an introduction to any of that, I recommend you read an introduction to scripting (shameless plug). If you are unfamiliar with Perl hashes and arrays, you should brush up, because they are important when managing Plist files.

As I was pulling all of this together, I had to make some assumptions. I decided that knowing Cocoa wouldn't be a prerequisite for this series. If you're Cocoa-comfortable, you might want to skip the sections subtitled "Object-Oriented for Dummies". If you do not know Cocoa, I will cover the very minimum; enough for you to understand the code in the article, but certainly not enough for you to start writing your own Cocoa code.

Cocoa and Perl

Here is a simple "Hello World" program written in Cocoa.


#import <foundation/Foundation.h>

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // These are the 4 lines we care about:
    NSString *s1 = [NSString stringWithCString:"Hello "];
    NSString *s2 = [[NSString alloc] initWithCString:"World"];
    NSString *s3 = [s1 stringByAppendingString:s2];
    printf ("%s\n", [s3 UTF8String]);

    [s2 release];

    [pool release];
    return 0;
}

Here is a Perl script that calls the same Cocoa functions or methods using the PerlObjCBridge.


#!/usr/bin/perl

use Foundation;

# These are the lines we care about:These
$s1 = NSString->stringWithCString_("Hello ");
$s2 = NSString->alloc()->initWithCString_("World");
$s3 = $s1->stringByAppendingString_($s2);
printf "%s\n", $s3->UTF8String();

$s2->release();

Obvious Differences

The Cocoa code begins with a line of code that imports the Foundation framework.


#import <foundation/Foundation.h>

For any Perl script that contain Cocoa code, include the PerlObjCBridge with this line:


use Foundation;

Second, the Cocoa code has the main function and creates and releases the NSAutoreleasePool. You do not need any of this in Perl.

Third, you will notice that the Cocoa code uses brackets and the Perl code does not. The Perl arrow operators replace the brackets. In Cocoa, the brackets are used to show the flow of execution. For example, [[NSString alloc] initWithString:@"World"] shows that [NSString alloc] is executed first because it is the innermost code. The next code that gets executed is initWithString:@"World".

The Perl arrow operator shows the same thing. For example, NSString->alloc()->initWithCString_("World") shows that NSString->alloc() is executed before initWithCString_("World").

Fourth, Cocoa uses colons (:) and Perl uses underscores (_). Colons let Cocoa know that a parameter is expected. In Perl, those colons are replaced with underscores.

Also, in Cocoa, the name of a method consists of multiple words; a word for each parameter. Here is an example of the 10.4 stringWithCString:encoding: method (this method only works in 10.4).


[NSString stringWithCString:"Hello " encoding:NSASCIIStringEncoding]

In Perl, the words are combined into one word separated by underscores, and the parameters are listed in parentheses. So stringWithCString: is combined with encoding: to become stringWithCString_encoding_, and "Hello " and NSASCIIStringEncoding are moved into the parentheses. The order also matters. The first word and the first parameter go together.


NSString->stringWithCString_encoding_("Hello ", NSASCIIStringEncoding);

In Cocoa, you create each variable with "NSString *". You do not need to include this in Perl.

Working with Objects (Object-Oriented for Dummies, Part 1)

This section is intended for Perl programmers who do not know Cocoa. If you are comfortable with Cocoa, you may want to skim or just skip this section.


$s1 = NSString->stringWithCString_("Hello ");

This line creates an object and sets its value to "Hello ".

If you do not know what an object is, they are not that difficult to understand. Review what you already know. A variable holds a value. An array holds values that are accessed with a number index. A hash holds values that are accessed with string keywords. A subroutine is a section of reusable code that may take parameters and return a value.

An object is like a super variable. It can hold values, arrays, or hashes. What makes it even more super is that objects can also contain subroutines, or methods.

Take the NSString object. It holds one value, a string. It also contains methods. The method stringByAppendingString_() takes a parameter and it returns a value, just like any subroutine. Where is the stringByAppendingString_() code? It is located in the NSString object.

Methods usually do something with the value stored in the object. The method stringByAppendingString_() returns a new string that is a combination of the string in the object and the string in the parameter. The methods stringWithCString_() and initWithCString_() set the object's current value, but each is slightly different. In all cases, the methods deal with the object's value.

When creating objects, you always give the name of the object you are creating, followed by a creation method, called a "factory" or "class" method. Look at the NSString documentation and scroll down to the section titled "Method Types." Some methods are listed with a plus (+) before them, and some with a minus (-).

When creating an object, you must use a method with a plus. You only call those methods once for an object. Here is a list of methods that can create NSString objects:


+ stringWithFormat:
+ localizedStringWithFormat:
+ stringWithCharacters:length:
+ string
+ stringWithString:
+ stringWithCString:encoding:
+ stringWithUTF8String:
+ stringWithContentsOfFile:encoding:error:
+ stringWithContentsOfFile:usedEncoding:error:
+ stringWithContentsOfURL:encoding:error:
+ stringWithContentsOfURL:usedEncoding:error:

The alloc() method is also used to create an object. The difference between alloc() and the other creation methods is that alloc() requires you to call an init-type method immediately afterwards. The other creation methods do not need an init-type method called.

The other unique aspect about creating objects with alloc() is that you should release() the objects when you are done using them, or you will leak memory. If the object is only used once and the script performs only a few tasks and then exits, it is not serious if you do not release it. But if your script runs for a long time, or you use alloc() inside of a loop, you certainly want to release the objects when you are done with them, or your script will start using huge amounts of RAM. Here is a simplified example of creating and releasing an object with alloc() and release().


for ( $x=1; $x<100000; $x++ ) {
	$y=NSString->alloc()->initWithCString_("$x");
	# use $y here... release when you are done:
	$y->release();
}

If you did not release, then your script would take more memory than it needs. To play it safe, you could just avoid using alloc.

Once you create the object, you only want to call the methods listed with a minus; these are called "instance" methods.

Now let us look closely at the example.


$s1 = NSString->stringWithCString_("Hello ");
$s2 = NSString->alloc()->initWithCString_("World");
$s3 = $s1->stringByAppendingString_($s2);
printf "%s\n", $s3->UTF8String();

$s2->release();

The first two lines create an NSString, first using NSString->stringWithCString_(), and second, using NSString->alloc()->initWithCString_(). The object pointed to by $s2 is created with alloc(), so we release it when we are done using it.

The third way to create an object is used when you have an object that returns a new object. The third line in the code example shows this:


$s1->stringByAppendingString_($s2)

The result of this is a brand new NSString object. The method stringByAppendingString_() is just like any subroutine or function; it takes parameters and returns a value. The value just happens to be a reference to a newly created NSString object. The full definition on the NSString documentation page is:


- (NSString *)stringByAppendingString:(NSString *)aString

The first word at the beginning tells what the method returns: "(NSString *)", or an NSString object location. The asterisk means it is a pointer, or reference. So the value returned is a reference to a brand new NSString object.

I can then store the location of this new object in $s3.


$s3 = $s1->stringByAppendingString_($s2);

You will notice that $s3 was not alloced, and stringByAppendingString_() is an instance method (listed with a minus). Because stringByAppendingString_() created an object, you don't have to use alloc() or another factory or class method with $s3. But notice that $s1 was already created. You can't call stringByAppendingString_() on an object that hasn't been created.

Pointers and References (Object-Oriented for Dummies, Part 2)

Once you create your objects, the variables $s1, $s2, and $s3 store the locations of the objects. These variables are called pointers in Cocoa and are converted to references in Perl. Pointers and references cause endless confusion to new programmers. There are a few things to remember about them.

First, you have to talk about them differently. Instead of saying "modify the object $s1," you have to say "modify the object referenced by $s1." Sometimes it is easier to say the former, but this is inaccurate, because $s1 isn't the object. I only mention this because in this article, I'll often say, "The object referenced by." It is wordy, but more accurate.

However, if you are new to pointers and references, there wont be any serious harm to think or talk of $s1 as the object. In time, you will want to make the mental shift, but don't fret over it. Failure to make the mental shift isn't worth not using the code. Continue to think of $s1 as the object for now. Not much behavior really changes by $s1 being a pointer instead of the object.

Second, you do not directly access $s1 and $s2. These are all wrong:


$s3 = $s1 . "World";

or


$s3 = $s1 . $s2;

or


print $s3

or


print "s3 is $s3";

To interact with an object, you always use a method. For example, to combine $s1 and $s2, I used:


$s1->stringByAppendingString_($s2)

If I wanted to print $s3, I might try:


print $s3;

But I would get something like:


NSCFString=SCALAR(0x186e3c8)

Oops! That is the memory location of the object! Instead, I can use the method UTF8String():


print $s3->UTF8String() . "\n";

Note that because UTF8String() does not take any parameters, it does not have an underscore.

Load a Plist File

What exactly is a Plist file? You may think it is an XML file, but that is not true, especially now that many 10.4 Plist files are saved in binary format; not XML at all. Plist files store different data types. They can have two container data types: array and dictionary. A dictionary is the same as a Perl hash. The base container of a Plist file is almost always a dictionary.

Both arrays and dictionaries can contain mixed data types such as string, int, float, boolean, date, data, or even a nested array or dictionary. These data types match the Cocoa objects NSDictionary, NSArray, NSString, NSNumber, NSDate, and NSData.

If you desire more information about Plist files, you can read Apple's Plist documentation.

When the base class of a Plist file is a dictionary, the base Cocoa class will be NSDictionary. It is possible to have an NSArray as the base class, but I have not noticed any Plist files with that base class.

Conveniently for us, NSDictionary has a method that loads a Plist file from disk.


$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );

The method dictionaryWithContentsOfFile_() is a factory or class method, so you can use it to create the object. The object will be filled with the contents of the file /Library/Preferences/SystemConfiguration/preferences.plist.

If you look at the documentation for dictionaryWithContentsOfFile_(), you will notice it takes an NSString. But we are passing it a Perl string. For convenience, a Perl scalar variable is converted to an NSString object when the method expects an NSString object.

The file you load must be a valid Plist file or else $plist will be set to the nil object. If there is a Cocoa error, then $plist will be undefined. You should check the value of $plist right after creating it just to make sure it has loaded. First, test to see if there was a Cocoa error with if ( $plist ). Then test for nil. The nil value is not empty or set to 0. So if ( $plist ) will be true if $plist is nil. To test for the nil object, use if ( $$plist ). When $plist is nil, this test will be false. Here is an example showing both tests:


#!/usr/bin/perl

use Foundation;

$file = "/bad/path.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
if ( ! $plist or ! $$plist ) {
  print "One of the following failed: $plist - $$plist\n";
}

Getting Plist Values

Because $plist is a Cocoa object, you can't use this code to print its value:


$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
print $plist; # this will print: NSCFDictionary=SCALAR(0x180b5a8)

To get a useful printout of $plist, use the Cocoa method description(). This method will print out an ASCII-formated Plist structure, which is easily readable. The result of the description() method is an NSString, so you must convert it to a UTF8String.


$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
print $plist->description()->UTF8String() . "\n";

Printing out an NSDictionary, especially when it is as big as the network preferences, isn't very useful in a Perl script, but it makes debugging easier.

Getting the value at the root of the Plist is fairly easy. You use the method objectForKey_().


$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
$value = $plist->objectForKey_("CurrentSet");
print $value->description()->UTF8String() . "\n";

Now $value is something we can use. We can print it and we can make decisions based on its value. The object you get back can be a string, number, Boolean, date, or data, and we can read the values by using ->description()->UTF8String(). Or a value might be a dictionary or an array. If it is a dictionary or an array, you can continue to read the values.

To avoid the clutter of using ->description()->UTF8String(), we will just wrap that in a subroutine:


sub perlValue {
  my $object = ( @_ );
  return $object->description()->UTF8String();
}

Using a Library File

Throughout these two articles, several subroutines are going to be shown. Examples will use these subroutines but not show the subroutine definitions, as that would make for a long article with lots of duplicated text. An example is the subroutine just defined: perlValue. That subroutine will be used in just about every example from here on. You either want to paste the subroutine code into each example script, or you can define a library file that contains the subroutine definition.

The easiest way to create a library is to paste the subroutine code into a file, and put a 1; at the end of the file, like this:


use Foundation;

sub perlValue {
  my ( $object ) = @_;
  return $object->description()->UTF8String();
}

# more subroutines go here...

1;

In the scripts that use the library, you use the folder that contains the script, and you require the script. Suppose you named the file perlplist.pl and saved it in your Desktop folder. You would put this code into the scripts that use the subroutines.


use lib "/Users/yourname/Desktop/";
require "perlplist.pl";

The first example in the next section shows how to do this in detail.

Getting Nested Plist Values

If the object returned from your first read attempt is a dictionary, you continue to use objectForKey_() on the results to read the nested dictionary. If the object returned is an array, you read it by using objectAtIndex_(). If you use objectForKey_() on an array, or objectAtIndex_() on a dictionary, you will get an error. Try this code:


#!/usr/bin/perl

use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue

$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
$value = perlValue( $plist->objectAtIndex_(1) );

If you get something like this error:

Can't locate perlplist.pl in @INC (@INC contains: /Users/yourname/Desktop/ ...snip... at test line 5.

That means your library file was not found. Make sure the path to your library file is correct.

You should get this error by calling a method that doesn't exist in the object:

2005-07-22 00:46:33.619 perl[2076] *** -[NSCFDictionary objectAtIndex:]: selector not recognized [self = 0x304fe0] ...snip... **** PerlObjCBridge: dying due to NSException

To avoid this, you can check the type of object returned by using isKindOfClass_() and class.


#!/usr/bin/perl

use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue

$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
$object = $plist->objectForKey_("System");
if ( $object->isKindOfClass_( NSArray->class ) ) {

  print "$object is a array\n";

  # if we want to read it, we use:
  $object = $plist->objectAtIndex_( $index );

} elsif ( $object->isKindOfClass_( NSDictionary->class ) ) {

  print "$object is a dictionary\n";

  # if we want to read it, we use:
  $ object = $plist->objectForKey_( $key );

} else {

  print "$object is something else, print its value:\n";
  print perlValue( $object ) . "\n";

}

After running the above example, you should get:


NSCFDictionary=SCALAR(0x180eaa0) is a dictionary

If you ask for an index or a key that does not exist, you will get back nil. So you will always want to test the return value for both a Cocoa error and nil:


$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
$object = $plist->objectForKey_("some key");
if ( ! $object or ! $$object ) {
  print "Object did not exist\n";
}

Using everything shown up to this point, we can actually write a subroutine that will get nested objects for us. The following subroutine takes an array or dictionary, and then a list of keys or indexes to read. It will traverse the array or dictionary given to it, using the keys or indexes, until it reaches the last one. At that point, it returns either the object found; nil, if it could not find what you asked for; or nothing, if there was another problem. Here is the subroutine with an example reading a nested object. You may want to add this subroutine to your library file. It will be used in the second article.


#!/usr/bin/perl

use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue

sub getPlistObject {
  my ( $object, @keysIndexes ) = ( @_ );
  if ( @keysIndexes ) {
    foreach my $keyIndex ( @keysIndexes ) {
      if ( $object and $$object ) {
        if ( $object->isKindOfClass_( NSArray->class ) ) {
          $object = $object->objectAtIndex_( $keyIndex );
        } elsif ( $object->isKindOfClass_( NSDictionary->class ) ) {
          $object = $object->objectForKey_( $keyIndex );
        } else {
          print STDERR "Unknown type (not an array or a dictionary):\n";
          return;
        }
      } else {
        print STDERR "Got nil or other error for $keyIndex.\n";
        return;
      }
    }
  }
  return $object;
}

$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSDictionary->dictionaryWithContentsOfFile_( $file );
if ( $plist and $$plist) {

  $computerName = getPlistObject( $plist, "System", "System", "ComputerName" );
  if ( $computerName and $$computerName ) {

    print perlValue( $computerName ) . "\n";

  } else {
    die "Could not find the value.\n";
  }
} else {
  die "Error loading file.\n";
}

The $plist variable holds the location to the dictionary loaded with the computer network settings, which is a very large file. Using the getPlistObject subroutine, these are the only lines in $plist that we care about:


<dict>
	<key>System</key>
	<dict>
		<key>System</key>
		<dict>
			<key>ComputerName</key>
			<string>Your computer name</string>
		</dict>
	</dict>
</dict>

After calling getPlistObject, $computerName will point to an NSString with the value of your computer's name.

Final Thoughts

This article has covered a lot of ground. If you are new to Cocoa, your head might be spinning with all of the object-oriented jargon. However, we have only touched the ground as far as managing Plist files. The best is yet to come! The second article will cover changing Plist values, saving the changes, scanning Plist files, and creating them from scratch. Do stay tuned in!

James Reynolds is a member of the University of Utah's Student Computing Labs Mac Group. His main duty is the deployment of Mac OS X. Most of his responsibilities include the OS customizations, scripts, and security of the Mac OS X lab and kiosk computers supported by SCL.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.