This article is the second part of a two-part series showing how to manage Plist files with Perl using the PerlObjCBridge included with Mac OS X since version 10.3. The first article included a brief Cocoa introduction, an explanation about how the bridge works, showed how to load Plist files from disk, how to read Plist values, and ended with a shortcut subroutine that makes reading nested values a snap. This article builds on that information. If you haven't read the first tutorial, you should before proceeding here, especially if you are unfamiliar with Cocoa.
This piece goes into much more detail on managing Plist files. You'll change some values and save the altered Plist file back to disk.
Then you'll loop over entries in a Plist file, get a dump of the NetInfo database, and print all the users in the database using Perl foreach loops. To do that you'll convert the Cocoa dictionaries and arrays to Perl hashes and arrays.
Finally, you'll create a Plist file from scratch, build the sample Xgrid cal job listed in the Xgrid man page by creating the structure using Perl hashes and arrays, and then convert them to Cocoa equivalents.
In the first article you set up a library file so you can store common subroutines in it, and use it in the scripts without having to duplicate the code. I showed you how to do this so that the examples don't duplicate code, namely the perlValue and getPlistObject subroutines. The examples in this article will continue to use these subroutines.
To sum up the process of using a library file, create a file at /Users/yourname/Desktop/perlplist.pl (always change "yourname" to your username) and include this text:
use Foundation;
sub perlValue {
my ( $object ) = @_;
return $object->description()->UTF8String();
}
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;
}
# more subroutines go here...
1;
In the scripts that use the subroutines, include this text at the top of the file:
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue and getPlistObject
If you do not use a library file, you will need to include these subroutines in your scripts in order to use them.
If you ever get an error that looks like one of the following, it is because the path to your library is incorrect or because the subroutine is missing from the library.
Can't locate perlplist.pl in @INC
(@INC contains: /Users/yourname/Desktop/ ...snip... .) at test line 5.
Undefined subroutine &main::getPlistObject called at test line 13.
Now let's jump right back into Plist files by changing a value and saving the Plist back to disk.
But first, a word of caution. Changing the Plist file of a running application is strongly discouraged since an application will not know to reread the file, and in fact the application could overwrite your changes. You could also accidentally clobber the file you are saving by accidentally overwriting something with the wrong data type or deleting nested containers. For example, it is possible to overwrite the root dictionary with an empty dictionary.
We will read the computer's network preferences.plist file, but we don't want to change it. Instead, we will save the changed version to the Desktop. If you want to make your changes take effect, you could make a backup of the original file and move the changed file into its place and reboot the computer, or find another way to force the system to reread the preferences.
Now, NSDictionary and NSArray can not have their values changed once they are set. These are optimized objects that you can use if you know you don't need to change them. On the other hand, if you know you need to change their values, then you use the mutable versions: NSMutableDictionary and NSMutableArray.
The mutable versions inherit the methods from the nonmutable versions. So you can call the same methods that are in the nonmutable versions, like dictionaryWithContentsOfFile_(), and writeToFile_atomically_().
In addition, these versions have extra methods that allow their values to be changed, like setObject_forKey_() for dictionaries and replaceObjectAtIndex_withObject_() for arrays. There are many more methods you can use to manipulate dictionaries and arrays. See Apple's NSMutableDictionary and NSMutableArray documentation to see the available methods.
Notice that setObject_forKey_() has two underscores, so it requires two parameters. The first parameter is the object, the second is the key. The method replaceObjectAtIndex_withObject_() also requires two parameters, the first is the index, and the second is the object.
$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSMutableDictionary->dictionaryWithContentsOfFile_( $file );
$plist->setObject_forKey_("some object", "some key");
print perlValue( $plist ) . "\n";
When adding objects to an array, replaceObjectAtIndex_withObject_() and insertObject_atIndex() must have indexes that do not exceed the size of the array. The former method will replace the object at the given index, the later method will make room for the new object by moving the objects in its way. To insert an object to the end of the array, just use addObject_().
When adding objects to a dictionary with setObject_forKey_(), the key-object pair will be created if the key does not exist. In the example above, since "some key" did not exist, the key-object pair is added to the dictionary. If a key-object pair already exists in the Plist, then the old object in the Plist is replaced with the new object. In the following example, new objects are added for keys that already exist. This example essentially clobbers all the settings. The changes are not saved to disk, so it is safe.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue
$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSMutableDictionary->dictionaryWithContentsOfFile_( $file );
if ( $plist and $$plist) {
$plist->setObject_forKey_("clobber one", "CurrentSet");
$plist->setObject_forKey_("clobber two", "NetworkServices");
$plist->setObject_forKey_("clobber red", "Sets");
$plist->setObject_forKey_("clobber blue", "System");
print perlValue( $plist ) . "\n";
} else {
die "Error loading file.\n";
}
|
Related Reading Mac OS X Tiger for Unix Geeks |
|
To create a key-object pair inside of a nested dictionary, we first have to get the location of the nested dictionary we want to change. We can get it using the shortcut getPlistObject subroutine. If we wanted to change the ComputerName, the following is a reduction of the Plist structure showing the dictionary that we want to grab. It is nested in two other dictionaries.
<dict>
<key>System</key>
<dict>
<key>System</key>
<dict>
<key>ComputerName</key>
<string>Your computer name</string>
<key>ComputerNameEncoding</key>
<integer>0</integer>
</dict>
</dict>
</dict>
Using the subroutine getPlistObject, we can grab the correct dictionary with this syntax:
getPlistObject( $plist, "System", "System" )
The next example will show the entire process, grabbing the dictionary and changing the computer name.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue and getPlistObject
$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSMutableDictionary->dictionaryWithContentsOfFile_( $file );
if ( $plist and $$plist) {
# this returns the dict that contains ComputerName
$computerNameParent = getPlistObject( $plist, "System", "System" );
if ( $computerNameParent and $$computerNameParent ) {
print "Original dictionary: " . perlValue( $computerNameParent ) . "\n";
# set the key ComputerName
$computerNameParent->setObject_forKey_("New name", "ComputerName");
print "Changed dictionary: " . perlValue( $computerNameParent ) . "\n";
# optionally print the whole thing:
#print perlValue( $plist ) . "\n";
} else {
die "Could not find the value.\n";
}
} else {
die "Error loading file.\n";
}
The code will print the contents of ComputerName's dictionary. I get:
Original value: {ComputerName = "Firebolt"; ComputerNameEncoding = 0; }
Changed value: {ComputerName = "New name"; ComputerNameEncoding = 0; }
When replacing the object for the key "ComputerName," the change is also visible inside of $plist also. If you uncomment the optional print statement you can see the changed value at the bottom of the long printout:
...snip...
System = {ComputerName = "New name"; ComputerNameEncoding = 0; };
};
}
Now, what if you wanted to change the value in an array? You would use replaceObjectAtIndex_withObject_. Here is the path of the preferences Plist that we will traverse.
<dict>
<key>Sets</key>
<dict>
<key>0</key>
<dict>
<key>Network</key>
<dict>
<key>Global</key>
<dict>
<key>IPv4</key>
<dict>
<key>ServiceOrder</key>
<array>
<!-- snip -->
</array>
</dict>
</dict>
</dict>
</dict>
</dict>
</dict>
Here is the code that will traverse the Plist and set the first array element.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue and getPlistObject
$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSMutableDictionary->dictionaryWithContentsOfFile_( $file );
if ( $plist and $$plist) {
$array =
getPlistObject( $plist, "Sets", "0", "Network", "Global", "IPv4",
"ServiceOrder" );
if ( $array and $$array ) {
print "Original array: " . perlValue( $array ) . "\n";
$array->replaceObjectAtIndex_withObject_(0, "New value");
print "Changed array: " . perlValue( $array ) . "\n";
} else {
die "Could not find the value.\n";
}
} else {
die "Error loading file.\n";
}
|
We can write a shortcut subroutine that behaves nearly the same as getPlistObject. However, this one treats the last two parameters differently. It takes the second to last parameter and uses it as the key or index to set. It takes the last parameter and uses it for the object to set.
If we are adding an object to an array and the index we specify is higher than the number of elements in the array, the subroutine will just add the object to the end of the array.
Here is the subroutine and an example changing the same setting as above.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue and getPlistObject
sub setPlistObject {
my ( $plistContainer, @keyesIndexesValue ) = ( @_ );
my $objectToSet = pop ( @keyesIndexesValue );
my $keyIndex = pop ( @keyesIndexesValue );
my $parentContainer = getPlistObject ( $plistContainer,
@keyesIndexesValue );
if ( $parentContainer and $$parentContainer ) {
if ( $parentContainer->isKindOfClass_( NSArray->class ) ) {
if ( $keyIndex > $parentContainer->count -1 ) {
$parentContainer->addObject_( $objectToSet );
} else {
$parentContainer->replaceObjectAtIndex_withObject_( $keyIndex,
$objectToSet );
}
} elsif ( $parentContainer->isKindOfClass_( NSDictionary->class ) ) {
$parentContainer->setObject_forKey_( $objectToSet, $keyIndex );
} else {
print STDERR "Unknown parent container type.\n";
}
} else {
print STDERR "Could not get value specified by @keyesIndexesValue.\n";
}
}
$file = "/Library/Preferences/SystemConfiguration/preferences.plist";
$plist = NSMutableDictionary->dictionaryWithContentsOfFile_( $file );
if ( $plist and $$plist) {
# set the object.
setPlistObject( $plist, "Sets", "0", "Network", "Global",
"IPv4", "ServiceOrder", 0, "1234" );
my $array = getPlistObject( $plist, "Sets", "0", "Network",
"Global", "IPv4", "ServiceOrder" );
print perlValue( $array );
} else {
die "Error loading file.\n";
}
If you change the second to last value, 0, to some other value, you can see that the index of "1234" changes in the array. And if you make it larger than the original array, it will be just added to the end.
Once we have changed our Plist, we can save it. Just add this line to save it:
$plist->writeToFile_atomically_( "/tmp/preferences.plist", "0" );
If you want to save to the desktop and you don't want to use the full path, you can use the relative path with the stringByExpandingTildeInPath() method:
my $relativePath = NSString->stringWithFormat_( "~/Desktop/preferences.plist" );
my $file = $relativePath->stringByExpandingTildeInPath();
$plist->writeToFile_atomically_( $file, "0" );
Now we can read and change Plist files. There are two more tasks. We can scan an entire Plist file, and we can create one from scratch.
At some point you may want to loop through every value of an array or hash. In Perl, you can scan arrays and hashes using foreach or while loops like these examples:
@array = (1,2,3);
foreach $i ( @array ) {
print "$i\n";
}
%hash = ( key=>"value", key2=>"value2");
while ( my ( $key, $value ) = each( %hash ) ) {
print "Key: " . $key . "\n";
print "Value: " . $value . "\n";
}
|
We can do the same thing in Cocoa. Use the objectEnumerator() or keyEnumerator() methods on an array or dictionary to get an NSEnumerator object. Then use the nextObject() method on the NSEnumberator object to get the each object or key. Let's loop through the root of the NetInfo database and print each key.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue
# dump netinfo database to a temp file
# (the sed is to add missing ";" to some ")")
system "/usr/bin/nidump -r / -t localhost/local |
sed -e \"s/)\$/);/g\" > /tmp/netinfo_db";
my $plist =
NSDictionary->dictionaryWithContentsOfFile_("/tmp/netinfo_db");
my $enumerator = $plist->keyEnumerator();
my $key;
while ( $key = $enumerator->nextObject() and $$key ) {
printf "Key: %s\n", perlValue( $key );
}
When nextObject reaches the last list item, it will return the nil object. We must check for nil using "$$key" or else the loop will repeat forever. "$$key" will return the value of the nil object (which is 0), which will stop the loop.
If we wanted to use Perl's foreach to scan the Plist files, we can use the following subroutines to convert Cocoa dictionaries and arrays into Perl hashes and arrays.
If we convert all Cocoa float, int, boolean, date, and data objects to Perl scalars using description()->UTF8String() we will not be able to tell one data type from another. A Cocoa int with the value of 1, a Cocoa boolean with the value of true, and a Cocoa string with the value of "1" will all look the same once converted. You might want to leave them as Cocoa objects if you need to know the difference between an int 1, a boolean true, or a string "1". The following subroutines have the option to convert all values to Perl or not.
Here is an example that uses the two subroutines to convert the netinfo database to Perl equivalents. The nidump command outputs ASCII Plist, which does not show data types. So this example converts every value to Perl scalars. It will print each username and the home folder for the user.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue
sub perlArrayFromNSArray {
my ( $cocoaArray, $convertAll ) = ( @_ );
my @perlArray = ();
my $enumerator = $cocoaArray->objectEnumerator();
my $value;
while ( $value = $enumerator->nextObject() and $$value ) {
if ( substr ( ref ( $value ), 0,9 ) eq "NSCFArray" ) {
my @newarray = perlArrayFromNSArray( $value, $convertAll );
push (@perlArray, \@newarray);
} elsif ( substr ( ref ( $value ), 0,14 ) eq "NSCFDictionary" ) {
my %newhash = perlHashFromNSDict( $value, $convertAll );
push (@perlArray, \%newhash);
} else {
push ( @perlArray, $convertAll ? perlValue( $value ) : $value );
}
}
return @perlArray;
}
sub perlHashFromNSDict {
my ( $cocoaDict, $convertAll ) = ( @_ );
my %perlHash = ();
my $enumerator = $cocoaDict->keyEnumerator();
my $key;
while ( $key = $enumerator->nextObject() and $$key ) {
my $value = $cocoaDict->objectForKey_($key);
if ( $value ) { # check to make sure an object was set
if ( substr ( ref ( $value ), 0,9 ) eq "NSCFArray" ) {
my @newarray = perlArrayFromNSArray( $value, $convertAll );
$perlHash{ perlValue( $key ) } = \@newarray;
} elsif ( substr ( ref ( $value ), 0,14 ) eq "NSCFDictionary" ) {
my %newHash = perlHashFromNSDict( $value, $convertAll );
$perlHash{ perlValue( $key ) } = \%newHash;
} else {
$perlHash{ perlValue( $key ) } =
$convertAll ? perlValue( $value ) : $value;
}
}
}
return %perlHash;
}
# dump netinfo database to a temp file
# (the sed is to add missing ";" to some ")")
system "/usr/bin/nidump -r / -t localhost/local |
sed -e \"s/)\$/);/g\" > /tmp/netinfo_db";
my $plist = NSDictionary->dictionaryWithContentsOfFile_("/tmp/netinfo_db");
# convert the entire NSDictionary & its contents to Perl
my %nidb = perlHashFromNSDict( $plist, 1 );
# No more Cocoa is used from here on.
my $childArray = $nidb{ "CHILDREN" }; # get the main array
foreach $childDict ( @$childArray ) {
if ( $$childDict{ "name" }[0] eq "users" ) { # find the users array
my $userArray = $$childDict{ "CHILDREN" };
foreach $userDict ( @$userArray ) { # loop through the users array
my $userName = $$userDict{ "name" }[0];
my $userHome = $$userDict{ "home" }[0];
print "$userName $userHome\n";
}
}
}
The netinfo database stores all values in arrays. This is why the example gets the first index array value using [0].
$$childDict{ "name" }[0]
Also, the two conversion subroutines nest each array or hash in another. When accessing a nested array or hash, or a value in a nested array or hash, you add an extra "$," like this:
@$array # accessing an array
%$hash # accessing a hash
$$array[ $i ] # getting the value from a nested array
$$hash{ "key" } # getting the value from a nested hash
In the above example, the only non-nested container is the parent dictionary, or %nidb. Once we dig into it, everything will be nested and require the extra dollar sign. To learn more about nested arrays and hashes, read the perlref documentation.
|
The last task we want to perform is creating a Plist from scratch. There are several ways we can do this. We could create the base dictionary and add each item to the dictionary one by one.
The more common method is to build the Plist starting with the deepest objects, then their parent containers, one by one, until the last container you build is the root dictionary.
Since the deepest objects are ints, strings, boolean, etc., let's go over creating those objects. Then we will cover creating the dictionary and array containers.
When adding Perl scalar variables to NSArrays or NSDictionaries, they are automatically converted to NSString objects. If we want the int, float, boolean, date, or data objects, we must create an object of that type using the Cocoa factory methods. Here are several shortcut subroutines that will create and return each type of object, except for NSData.
sub cocoaInt {
return NSNumber->numberWithLong_( $_[0] );
}
sub cocoaBool {
return NSNumber->numberWithBool_( $_[0] );
}
sub cocoaFloat {
return NSNumber->numberWithDouble_( $_[0] );
}
sub cocoaDate {
return NSDate->dateWithTimeIntervalSince1970_( $_[0] );
}
Each of these subroutines will return a Cocoa object with the specified value. There are many other ways to create these objects, but these methods should be easy to use and will probably suffice. We can just put these in our library file.
Now on to the container classes. To create an empty dictionary, use:
$dict = NSMutableDictionary->dictionary();
To create a dictionary with one object, use:
$dict = NSMutableDictionary->dictionaryWithObject_forKey_("Obj", "Key");
Then we want to add objects to these dictionaries using setObject_forKey_() as described in the section, "Changing a Plist Value," earlier in the article.
$plist->setObject_forKey_("some object", "some key");
Adding each object one-by-one can be tedious. It would be nice to create a fully populated dictionary at one time. NSDictionary has the method dicitonaryWithObjects:forKeys: (which allows us to create a fully populated dictionary) but unfortunately this method does not work with the bridge. (See the PerlObjCBridge man page on varargs.)
To create an NSArray or NSDictionary all at once, we can write subroutines that add the objects one-by-one for us. In fact, because the parameter is either an array or a hash, the subroutines just convert a Perl hash or array into the Cocoa equivalent. We can just put these in our library file.
sub cocoaDictFromPerlHash {
my ( %hash ) = ( @_ );
my $cocoaDict = NSMutableDictionary->dictionary();
while ( my ( $key, $value ) = each( %hash ) ) {
if ( defined $value ) {
if ( ref ( $value ) eq "ARRAY" ) {
$cocoaDict->setObject_forKey_( cocoaArrayFromPerlArray( @$value ),
$key );
} elsif ( ref ( $value ) eq "HASH" ) {
$cocoaDict->setObject_forKey_( cocoaDictFromPerlHash( %$value ),
$key );
} elsif ( ref ( $value ) eq "SCALAR" ) {
$cocoaDict->setObject_forKey_( $$value, $key );
} elsif ( substr ( ref ( $value ), 0,4 ) eq "NSCF" ) {
$cocoaDict->setObject_forKey_( $value, $key );
} else {
$cocoaDict->setObject_forKey_( "$value", $key );
}
} else {
print STDERR "The value was not defined for $key!\n";
}
}
return $cocoaDict;
}
sub cocoaArrayFromPerlArray {
my ( @perlArray ) = ( @_ );
my $cocoaArray = NSMutableArray->array();
foreach my $value ( @perlArray ) {
if ( defined $value ) {
if ( ref ( $value ) eq "ARRAY" ) {
$cocoaArray->addObject_( cocoaArrayFromPerlArray( @$value ) );
} elsif ( ref ( $value ) eq "HASH" ) {
$cocoaArray->addObject_( cocoaDictFromPerlHash( %$value ) );
} elsif ( ref ( $value ) eq "SCALAR" ) {
$cocoaArray->addObject_( $$value );
} elsif ( substr ( ref ( $value ), 0,4 ) eq "NSCF" ) {
$cocoaArray->addObject_( $value );
} else {
$cocoaArray->addObject_( "$value" );
}
} else {
print STDERR "The value was not defined!\n";
}
}
return $cocoaArray;
}
|
Here is a simple example that creates a dictionary filled with an int, boolean, string, float, and date.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for cocoaInt, etc, and cocoaDictFromPerlHash
my %hash = (
int=>cocoaInt( 1 ),
boolean=>cocoaBool( 1 ),
string=>"1",
float=>cocoaFloat( 1.0 ),
date=>cocoaDate( 1 ),
);
my $plist = cocoaDictFromPerlHash( %hash );
$plist->writeToFile_atomically_( "/tmp/example.xml", "0" );
open ( FILE, "</tmp/example.xml" ) or die "Can't open\n";
print <FILE>;
close ( FILE );
Here is an example that creates a Plist file that we can use to submit a simple Xgrid batch job. To see an XML representation of this Plist file, look at the Xgrid 1.0 man page.
#!/usr/bin/perl
use Foundation;
use lib "/Users/yourname/Desktop/";
require "perlplist.pl"; # for perlValue
my @arguments = ( "6", "2005" ); # start with the deepest objects first
my %task0 = (
command=>"/usr/bin/cal",
arguments=>\@arguments,
);
my %tasks = (
0=>\%task0,
);
# the last object is the parent dictionary
my %hash = (
name=>"Cal Job",
taskSpecifications=>\%tasks,
);
my $plist = cocoaDictFromPerlHash( %hash );
$plist->writeToFile_atomically_( "/tmp/sample_batch.xml", "0" );
system "xgrid -job batch /tmp/sample_batch.xml";
A final note, when nesting arrays or hashes in arrays or hashes, you must add a backslash:
arguments=>\@arguments
and
taskSpecifications=>\%tasks
I hope the Plist management subroutines shown in this article will be useful. I especially hope I have been able to expose Perl scripters to enough Cocoa that they can come up with more uses for the PerlObjCBridge. Cocoa is a wonderful language. Methods like dictionaryWithContentsOfFile_ and writeToFile_atomically_ are excellent examples of why you would want to include a little Cocoa in Perl scripts.
In learning the PerlObjCBridge, I was surprised how few code examples were publicly available. If you do come up with your own code, I encourage you to put the code online. If you desire to take Cocoa and Perl a step further, check out Camelbones.
Finally, I'd just like to thank Charles Parnot for his help with the article and understanding the PerlObjCBridge. And I'd like to thank David Kramer for giving me the idea that led to this article: using the PerlObjCBridge to parse the output of the Xgrid command-line tool, which is formatted as ASCII Plist.
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.