2006-06-12

I really like Cocoa. Almost every day, I find something new I like about it. However, when it surprises me, I can bang my head against a wall for days! I have been launching my image viewer from the command line and retrieving its arguments from NSProcessInfo. I'm getting close to packaging it up for others' use and I want it to work equally well for a command line user and a graphical UI user. This was harder than I expected.

The graphical launch is easy enough; the Application Kit provides plenty of support for that. Supporting command line options is the headache. Cocoa parses command line arguments and, for every filename, calls the application delegate's [application:openFile:] method. If Cocoa encounters an option it doesn't understand, it skips that option and the NEXT item on the command line! (It must assume the next item is an argument for the option.) So if my command line is "Viewer --option file1 file2 file3", the program will never see file1.

That should be easy to fix, right? I plugged in some code in main.m to preprocess the arguments (remove my options) before feeding them to NSApplicationMain(). There was no change in the behavior. I double-checked my code and tried different implementations. No change. It took a lot of builds, but I finally figured out that NSApplicationMain() doesn't use its arguments; it reads the argument list from NSProcessInfo (WHY DOES IT EVEN HAVE THE ARGUMENTS IF IT JUST IGNORES THEM??? Google tells me that it DID use them prior to OS X 10.2, so why did Apple change it? And WHY don't they mention this anywhere in the documentation!?)

Anyway, there is no standard way to modify the arguments in NSProcessInfo. Bill Bumgarner has a hack to modify them, but there is a way to do it that is more in the spirit of Objective-C, categories:

@interface NSProcessInfo (ProcessInfoArgumentListModification)
-(void) resetArguments: (char **) argv count: (int) argc;
@end
@implementation NSProcessInfo (ProcessInfoArgumentListModification)
-(void) resetArguments: (char **) argv count: (int) argc {
	NSMutableArray *args = [[NSMutableArray alloc] initWithCapacity: argc];
	int i;
	for (i = 0; i < argc; i++) [args addObject: [NSString stringWithUTF8String: argv[i]]];
	arguments = args;
}
@end

int main( int argc, char *argv[] ) {
	... modify argc,argv ...
	[[NSProcessInfo processInfo] resetArguments: argv count: argc];
	return NSApplicationMain(argc,argv);
}

Instead of tinkering with argv[], it would be nice to work with a mutable array. I thought a cleaner alternative might be to replace the original array with a mutable array and modify it in place:

@interface NSProcessInfo (ProcessInfoArgumentListModification)
-(NSMutableArray *) makeArgumentsMutable;
@end
@implementation NSProcessInfo (ProcessInfoArgumentListModification)
-(NSMutableArray *) makeArgumentsMutable {
	NSMutableArray *args = [[NSMutableArray alloc] initWithCapacity: [arguments count]];
	[args addObjectsFromArray: arguments];
	[arguments release];
	return arguments = args;
}
@end

However, that doesn't work because NSProcessInfo doesn't set its argument list prior to the NSApplicationMain() call! The first implementation must work because NSProcessInfo only builds its arguments list if the arguments member is NULL. If I set it first, NSProcessInfo will not change it. As a side effect, the first implementation will not leak memory because there is no previous argument list to release. My final implementation combines those ideas:

@interface NSProcessInfo (ProcessInfoArgumentListModification)
-(NSMutableArray *) makeMutableArguments: (char **) argv count: (int) argc;
@end
@implementation NSProcessInfo (ProcessInfoArgumentListModification)
-(NSMutableArray *) makeMutableArguments: (char **) argv count: (int) argc {
	NSMutableArray *args = [[NSMutableArray alloc] initWithCapacity: argc];
	int i;
	for (i = 0; i < argc; i++) [args addObject: [NSString stringWithUTF8String: argv[i]]];
	arguments = args;
	return args;
}
@end

int main( int argc, char *argv[] ) {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	NSMutableArray *args = [[NSProcessInfo processInfo] makeMutableArguments: argv count: argc];
	... modify args if needed ...
	[pool release];

	return NSApplicationMain(argc,  (const char **) argv);
}