Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Simple methods for date formatting and transcoding

There is no single-line method for converting between formatting date strings and date objects in Cocoa — the API opts for flexibility rather than simplicity. Unfortunately, this combines with documentation that omits, misdirects and occasionally misinforms, making NSDateFormatter one of the more confusing classes for new Cocoa programmers. In this post, I'll try to address some of the documentation issues and I'll present some methods that will turn NSDate into a formatted string or convert between date strings in a single method.

Default, locale-based date formatting

Before I get into date formatting strings (which is the real purpose of this post) I will quickly show you how to get a string from an NSDate:

NSDate *date = [NSDate date];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
NSString *dateString = [dateFormatter stringFromDate:date];

This code formats the string according to the user's preferences set in the International General Settings (iPhone) or System Preferences (Mac OS X).

Personally, I think this is verbose for such a common activity and normally use a category method on NSDate to reduce it:

@implementation NSDate (FormattedStrings)
- (NSString *)dateStringWithStyle:(NSDateFormatterStyle)style
{
    NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
    [dateFormatter setDateStyle:style];
    return [dateFormatter stringFromDate:self];
}
@end

This reduces the first code sample to:

 NSString *dateString = [[NSDate date] dateStringWithStyle:NSDateFormatterLongStyle];

If you need time strings instead of date strings, create a timeStringWithStyle: method by replacing the setDateStyle: invocation with setTimeStyle:.

I have read rants by other Objective-C programmers who hate seeing projects with hundreds of small categories for every little task.

Frankly, I've never understood the objection. In fact, I think lots of small categories used to choose the settings you prefer on top of common interfaces is essential for establishing consistent aesthetics for your program — your entire program will have the same style and you can easily update the aesthetic for the whole program by editing a single location.

An example would be to replace this dateStringWithStyle: method with a dateStringWithProjectStyle method that returns the appropriately configured string for use throughout your program. One of your projects might use NSDateFormatterLongStyle and the next might use a totally customized format (as I'll describe in the next sections) but you as a programmer can still invoke dateStringWithProjectStyle everywhere you need a string from an NSDate.

Date formatting documentation issues

At its heart, NSDateFormatter is very simple to use and yet it repeatedly baffles new users. I don't think this is really the fault of the API as much as the history behind it and the effect that it has had on the documentation.

Despite NSDateFormatterBehavior10_4 being the only date formatting you should ever use and the only style that should exist in the documentation, Apple's documentation has the following quirks:

  1. The actual syntax for NSDateFormatterBehavior10_4 is never given in the documentation and you can easily miss the links to Unicode Standard (tr35) which describe it.
  2. A majority of the pages in the date formatting documentation seem concerned with the old NSDateFormatterBehavior10_0 style formatter behavior even though this is functionally deprecated.
  3. The documentation for defaultFormatterBehavior claims NSDateFormatterBehavior10_0 is the default style but it is actually NSDateFormatterBehavior10_4 in Leopard and iPhoneSDK2.0.

Then, if you've been skimming through the documentation getting confused by the different styles, you may overlook one line at the top of the NSDateFormatter API reference page:

iPhone OS Note: iPhone OS supports only the modern 10.4+ behavior. 10.0-style methods and format strings are not available on iPhone OS.

All that documentation in the iPhone SDK concerned with the old NSDateFormatterBehavior10_0 style is completely meaningless — you can't use NSDateFormatterBehavior10_0 at all on the iPhone.

Adding to the iPhone frustration, NSDateFormatterBehavior10_0 will work in the simulator, causing headaches when code suddenly stops working on the device.

On Mac OS X, even though you can use NSDateFormatterBehavior10_0, it is deprecated for all practical purposes so you probably shouldn't. Annoyingly, since the documentation still claims NSDateFormatterBehavior10_0 is the default, you should explicitly set the formatter behavior to NSDateFormatterBehavior10_4 to remain safe — at least until the documented default is updated to formally match the actual default.

I don't mean to be cruel to Apple — documentation is difficult, time consuming and annoying — but I suspect these quirks in the documentation and behavior are responsible for a lot of confusion.

Date formatting syntax

Setting a date formatting string looks like this:

NSDate *date = [NSDate date];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setDateFormat:@"dd MMM, yyyy"];
NSString *dateString = [dateFormatter stringFromDate:date];

This will create a string of the format "26 May, 2009". The arrangement of data in this string is determined by the format string passed to the setDateFormat: method.

A quick summary of the date formatting options useable in this format string:

CharacterMatches/OutputsMultiples
yYear1, 2 or 4 'y's will show the value, 2 digit zero-padded value or 4 digit zero-padded value respectively
MMonth1, 2, 3, 4 or 5 'M's will show the value, 2 digit zero-padded value, short name, long name or initial letter months
dDay of Month1 or 2 'd's will show the value or 2 digit zero-padded value representation respectively.
EWeekday1, 2, 3, 4 or 5 'e's will show the value weekday number, 2 digit zero-padded value weekday number, short name, long name or initial letter respectively. Weekday numbers starts on Sunday. Use lowercase 'e' for weekday numbers starting on Monday.
aAM or PMNo repeat supported
hHour1 or 2 'h's will show the value or 2 digit zero-padded value representation respectively. Use uppercase for 24 hour time.
mMinute1 or 2 'm's will show the value or 2 digit zero-padded value representation respectively.
sSecond1 or 2 's's will show the value or 2 digit zero-padded value representation respectively.
zTimezone1, 2, 3 or 4 'z's will show short acronym, short name, long acronym, long name respectively. Use uppercase to show GMT offset instead of name — 1 or 2 digit zero-padded values shows GMT or RFC 822 respectively.

Case is important — using the wrong case can cause parsing to fail. Use lowercase by default for all values except Month.

Alphabetic and numeric characters should be enclosed in single quotes (asterisk character) if you want them to pass through the parser as literal characters. A double asterisk will pass through as a single asterisk (self escaping). Most other punctuation and spaces will go through as normal except backslash which works like a normal C-string escape character to handle tabs, newlines and other special characters.

For the complete specification, including the more obscure options that I haven't bothered to list, see Unicode Standard (tr35).

Date string transcoding

The date formatting options can also be used to parse strings. With parsing and output formatting, we can create a date string transcoder method in a category on NSDateFormatter:

+ (NSString *)dateStringFromString:(NSString *)sourceString
    sourceFormat:(NSString *)sourceFormat
    destinationFormat:(NSString *)destinationFormat
{
    NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
    [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
    [dateFormatter setDateFormat:sourceFormat];
    NSDate *date = [dateFormatter dateFromString:sourceString];
    [dateFormatter setDateFormat:destinationFormat];
    return [dateFormatter stringFromDate:date];
}

We can use this method to convert one date string (for example "2007-08-11T19:30:00Z") into another ("7:30:00PM on August 11, 2007") in the following manner:

NSString *inputDateString = @"2007-08-11T19:30:00Z";
NSString *outputDateString = [NSDateFormatter
    dateStringFromString:inputDateString
    sourceFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"
    destinationFormat:@"h:mm:ssa 'on' MMMM d, yyyy"];

Calendar stuff

The NSDateFormatter class is just for parsing and formatting strings. You shouldn't use it to build dates from their components or to decompose dates into components. For that task, use NSCalendar. It's a little outside the scope of this post but since I know it will come up, here's how to set an NSDate to the 26th of May, 2009 using NSCalendar:

NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease];
[components setDay:26];
[components setMonth:5];
[components setYear:2009];
NSCalendar *gregorian =
    [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSDate *date = [gregorian dateFromComponents:components];

To get the components from a date, use:

unsigned unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit;
NSDate *date = [NSDate date];
NSCalendar *gregorian =
    [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSDateComponents *components = [gregorian components:unitFlags fromDate:date];

Conclusion

As I've said a few times now, the actual API for date formatting in Cocoa is simple. The only reason why I consider it a topic worthy of a post is that I still see at least one question a week on StackOverflow and other forums asking why NSDateFormatter is failing in their code and the answer is normally because their code confuses the formatting behaviors, uses the wrong format specifiers or has otherwise misunderstood the required steps.

I hope what I've presented clears up these issues. Date formatting should be simple enough that you can write it once and never worry about it again.

Intercepting status bar touches on the iPhone

You can configure your iPhone applications so that a touch in the status bar will scroll a UIScrollView to the top. I'll show you how you can intercept this touch event to use status bar touches for other purposes. The sample application will show a hidden drawer that slides out from the status bar when you tap it.

Touches in the status bar

A lesser known user-interface feature on the iPhone is that touches in the status bar will usually scroll the main UIScrollView to the top, providing a quick way to scroll to the top of long documents.

This will work in your application when exactly one UIScrollView returns YES for the scrollsToTop property (YES is the default). If more than one UIScrollView returns YES for this property (or the UIScrollView's delegate returns NO from scrollViewWillScrollToTop:) the scroll to top functionality will be disabled.

That's the ordinary functionality but how do we achieve different functionality?

The HiddenDrawer sample appliction

hidden_drawer_screenshots.png

These screenshots show the HiddenDrawer sample application. When the status bar is tapped on the left, the hidden drawer animates out from under the status bar, resulting in the state shown on the right.

Stealing status bar touch events

The trickiest part of the sample application is detecting a touch in the status bar.

By implementing a custom setContentOffset:animated: method on a UITableView and setting a breakpoint in that method, you can see in the debugger stack that the UIApplicationsendEvent: method is invoked for status bar touches, so that's where we'll begin.

CustomApplication

Overriding UIApplication is extremely rare so I'll explain how to make it work. Once you create the subclass of UIApplication you need to tell the program to use that subclass. In Cocoa Senior (Mac OS X) you specify application subclasses in the Info.plist file. In Cocoa Touch, you specify custom application subclasses by name in the UIApplicationMain function in the main.m file:

int retVal = UIApplicationMain(argc, argv, @"CustomApplication", nil);
sendEvent:

The only method override we need in CustomApplication is sendEvent:. The difficult part is then working out from the UIEvent which events are status bar touch events — unfortunately, the allTouches method returns an empty array for status bar touches.

Instead, we delve into the secret GSEvent.

I previously accessed GSEvent in my post Synthesizing a touch event on the iPhone. In that post, I created a PublicEvent class and a fake GSEventProxy class to access the required fields. This time, I'm going to use a different approach and jump straight to the data I need.

- (void)sendEvent:(UIEvent *)anEvent
{
    #define GS_EVENT_TYPE_OFFSET 2
    #define GS_EVENT_X_OFFSET 6
    #define GS_EVENT_Y_OFFSET 7
    #define STATUS_BAR_TOUCH_DOWN 1015
    
    // Traverse from the UIEvent to the GSEvent to the type
    int *eventMemory = (int *)[anEvent performSelector:@selector(_gsEvent)];
    int eventType = eventMemory[GS_EVENT_TYPE_OFFSET];

    // Look for status bar touches by event type
    if (eventType == STATUS_BAR_TOUCH_DOWN)
    {
        // The next 6 lines aren't essential but if you want to know where the
       // touch coordinates live, here they are: 
        int xMemory = eventMemory[GS_EVENT_X_OFFSET];
        int yMemory = eventMemory[GS_EVENT_Y_OFFSET];

        typedef union {int intValue; float floatValue;} Int2Float;
        float x = ((Int2Float)xMemory).floatValue;
        float y = ((Int2Float)yMemory).floatValue;

        NSLog(@"Status bar down at %f, %f", x, y);
        
        // Send a message to the delegate to handle the action
        [(HiddenDrawerAppDelegate *)self.delegate toggleDrawer];
    }
    else
    {
        [super sendEvent:anEvent];
    }
}

You may be curious to know where the OFFSET values come from. The answer is that I spent a while staring at the raw memory values in the GSEvent object while deliberately causing status bar and other touch events — nothing fancier than that. It's tricky and unreliable. If it works at all in iPhoneSDK3.0, it'll be pure luck.

I also use a union here. This is because I step through memory as ints and in C, a basic cast from int to float causes a value conversion (I want a reinterpret, not a value conversion).

I also chose to suppress status bar touch events going through to the UITableView by the normal route. If you want to re-enable this behavior, you can take the [super sendEvent:anEvent]; line out of the else block and put it in the main method body.

Animating the drawer

The sendEvent: implementation above invokes the toggleDrawer method on the application's delegate.

All that's required is to animate the drawer's view in and push the table's view down:

drawerController = [[HiddenDrawerViewController alloc] init];

// Position the drawer below the status bar
CGRect drawerFrame = drawerController.view.frame;
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
drawerFrame.origin.x = statusBarFrame.origin.x;
drawerFrame.size.width = statusBarFrame.size.width;
drawerFrame.origin.y = statusBarFrame.origin.y + statusBarFrame.size.height;

// For the animation, move the drawer up by its own height.
drawerFrame.origin.y -= drawerFrame.size.height;

// Place the drawer and add it to the window
drawerController.view.frame = drawerFrame;
[window addSubview:drawerController.view];

// Start the animation
[UIView beginAnimations:nil context:nil];

// Move the table down
CGRect tableFrame = viewController.view.frame;
tableFrame.origin.y += drawerFrame.size.height;
viewController.view.frame = tableFrame;

// Move the drawer down
drawerFrame.origin.y += drawerFrame.size.height;
drawerController.view.frame = drawerFrame;

// Commit the animation
[UIView commitAnimations];

If you download the whole project, you'll see that there's also an animate up and remove branch that gets run if the drawerController already exists.

Conclusion

You can download the HiddenDrawer sample project (30kB) to see the whole application in action.

A hidden drawer under the status bar isn't necessarily something that every iPhone application should have but the obscure, secretive nature of it appeals to me.

The approach of determining which UIEvent we want by the type field in the GSEvent is a little precarious. Apple are free to change the structure of GSEvent at any time, which could cause your application to misbehave or crash so this type of code would need to be tested on each iPhone OS release to ensure that it still works.

Variable argument lists in Cocoa

This week I'll talk about methods that take variable numbers of arguments, also known as variadic methods. I'll show you the Objective-C syntax and implementation, give a quick rundown of the ways that Cocoa classes provide variable argument support and I'll also show you a way to fake va_list parameters to handle Cocoa's variadic method equivalents at runtime.

Variable arguments basics

Passing a variable number of arguments to a method is a convenient way to handle a list of variables that are in scope at compile time.

The Objective-C language handles variable arguments in the same way that Standard C does. Normally, you will encounter variable argument lists in one of two forms: "Format strings" or "Nil terminated lists".

Format strings

The traditional example for variable arguments are format strings. Format strings contain a number of "placeholders" (escape sequences starting with a "%" sign) that are replaced with data from variables when the format string and the variable arguments are passed to the method.

NSString *myString = [NSString stringWithFormat:
    @"Number %d, String: %@, Float: %g", 123, @"SomeString", 34.5];

This method is declared as follows:

+ (id)stringWithFormat:(NSString *)format, ...;

The "..." is the variable argument.

This method declares a first parameter but everything else is "variable". This does not mean optional. Instead, it means that a number of extra parameters will be required. Exactly what number and their types depends on how the method works.

The documented behavior for stringWithFormat:, is that it scans the format string and requires 1 variable argument for every escape sequence (in this case there are 3) and the type must match the specifier for each escape sequence (in this case, "d", "@" and "g" specify int, id and float/double).

Nil terminated lists

The other common type of variable argument list method is one that takes a list of objects terminated by nil.

In Cocoa, these are commonly used for constructing collection objects.

NSArray *myArray = [NSArray arrayWithObjects:@"One", @"Two", @"Three", nil];

The documented rule for this type of method is that the last argument must be nil.

Getting argument numbers wrong

The details of "Format strings" and "Nil terminated lists" reveal the difficulty with variable argument lists in Objective-C: knowing how many arguments exist — i.e. when to stop reading arguments.

Objective-C, like C, does not have a runtime argument count, nor does it pass runtime argument types. So the number and types of arguments must be determined by some other policy. In the case of these two examples, the argument count is, respectively: the number of escape sequences in the "Format string" or the number of arguments before the first nil. For "format strings" the types are determined by the escape sequences, for "Nil terminated" lists, the arguments are always pointers.

The penalty for getting variable argument list wrong can be severe: the compiler won't typically notice a problem and your code will crash or behave very strangely at runtime. The important lesson to learn is that you should only use variable arguments in a situation where the policy for numbers and types of arguments is very clear.

You can improve the compiler verification of variable argument lists by using the -Wformat compiler flag in GCC. See below for more about how this works.

Implementing variable arguments for your own methods

Lets look at the implementation for the example class:

@interface StringContainer : NSObject
{
    NSString *contents;
}
@end

Imagine we wanted a method that would set the contents string this class contains by concatenating a variable argument list of strings together.

In this case, we will use the nil terminated approach — the list of strings to concatenate will end with a nil, so we know when to stop reading.

The declaration for the method looks like this:

- (void)setContentByAppendingStrings:(NSString *)firstString, ...
    NS_REQUIRES_NIL_TERMINATION;

The NS_REQUIRES_NIL_TERMINATION part is a macro that tells the compiler that invocations of this method must include a nil-terminated list of arguments. Failure to nil-terminate the list will result in a compiler warning if -Wformat is enabled.

Annoyingly, -Wformat is not enabled by default. Double annoyingly, it is named "Typecheck Calls to printf/scanf" in the "GCC 4.0 Warnings" section of the "Build Settings" in Xcode, even though this setting affects the NS_REQUIRES_NIL_TERMINATION macro which is used commonly throughout all of Cocoa — far beyond printf and scanf.

The implementation of this method is as follows:

- (void)setContentByAppendingStrings:(NSString *)firstArg, ...
{
    NSMutableString *newContentString = [NSMutableString string];
    va_list args;
    va_start(args, firstArg);
    for (NSString *arg = firstArg; arg != nil; arg = va_arg(args, NSString*))
    {
        [newContentString appendString:arg];
    }
    va_end(args);
    
    [contents autorelease];
    contents = [newContentString retain];
}

The va_list, va_start, va_arg and va_end are all standard C syntax for handling variable arguments. To describe them simply:

  • va_list - A pointer to a list of variable arguments.
  • va_start - Initializes a va_list to point to the first argument after the argument specified.
  • va_arg - Fetches the next argument out of the list. You must specify the type of the argument (so that va_arg knows how many bytes to extract).
  • va_end - Releases any memory held by the va_list data structure.

Generally speaking, you can use this for loop for any variable argument situation where your arguments are all the same type. Other cases are a bit trickier but far less common — I'm sure you can work out how they would work if needed.

va_list in Cocoa

A number of classes in Cocoa have methods that take variable numbers of arguments. In most cases, these classes will also have an equivalent method that takes a va_list.

We can see an example of these va_list equivalents by looking at NSString. NSString declares the class method stringWithFormat:... (which takes a variable number of arguments) and NSString also declares the instance method initWithFormat:arguments: (where the arguments parameter is a va_list) which handles the equivalent behavior of stringWithFormat:....

These va_list methods are used in the situation where your class defines a method with a variable argument list and you need to pass those variable arguments into the Cocoa method. For example, if the StringContainer class listed above declared the method:

- (void)setContentsWithFormat:(NSString *)formatString, ...;

The implementation of this method would be as follows:

- (void)setContentsWithFormat:(NSString *)formatString, ...
{
    [contents autorelease];

    va_list args;
    va_start(args, formatString);
    contents = [[NSString alloc] initWithFormat:formatString arguments:args];
    va_end(args);
}

The va_list parameter allows us to pass our own variable argument list to the Cocoa method so that the Cocoa method can handle the arguments.

Creating a fake va_list

The va_list methods in Cocoa are helpful if you actually have a variable argument list.

There is another situation you many encounter though: you want to use a method like -[NSString initWithFormat:arguments:] using a runtime generated array of arguments. There is no NSString format method that takes an NSArray of arguments, so how could we handle a format string at runtime?

The answer lies in how va_list works. While GCC makes it very clear that va_list is "platform specific", the reality is that on Mac and iPhone Objective-C platforms, it is simply a byte buffer containing the arguments. In fact, if you've ever used an ABI inspection tool (like class-dump-x) on a method taking a variable argument list, you'll see that it is simply a char *.

To show how we can use this knowledge, consider the following method on the StringContainer class:

- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments;

If we assume that all of the variable arguments required fro the formatString are objects, then we can implement this method as follows:

- (void)setContentsWithFormat:(NSString *)formatString arguments:(NSArray *)arguments
{
    [contents autorelease];

    char *argList = (char *)malloc(sizeof(NSString *) * [arguments count]);
    [arguments getObjects:(id *)argList];
    
    contents = [[NSString alloc] initWithFormat:formatString arguments:argList];
    
    free(argList);
}

What I've done here is simply copied the NSArray to a C-style byte buffer and then passed that buffer to the initWithFormat:arguments: method.

If your arguments are not all Objective-C objects, you'd need to take greater care to assemble the byte buffer but the principles would be the same.

Some variadic notes

NSInvcation incompatibility

NSInvocation does not support variadic methods. I'm not sure why this is — maybe NSInvocation wants to avoid making assumptions about va_list, maybe it is because NSMethodSignature can't describe the storage requirements of variable arguments for NSInvocation or maybe the Cocoa developers have families and just wanted to go home early.

Variadic macros

You can also write variadic macros, much like variadic methods, by using the ##__VA_ARGS__ placeholder in your macros. See the macros near the bottom of my post titled Supersequent Implementation for an example of how this works. As discussed briefly in that article, the version of GCC used for iPhone development handles variadic macros slightly differently, so pay attention to the differences when targetting the iPhone platform.

Conclusion

Variadic methods require a degree more care than regular methods because variable argument lists have no "introspection" (you cannot ask for the number and type of arguments) — so their usage relies on documentation and implicit agreements between the sender and receiver.

In your own code, variadic methods should be used sparingly — passing variables in an NSArray or NSDictionary is safer (if slightly slower and syntactically more verbose) due to the fact that these classes do offer introspection.

When the implicit sender/receiver agreement is clear, variadic methods work well. They certainly make creating instances of NSArray, NSSet, NSDictionary easier and they are the only way to create a formatted NSString in a single invocation.

Invoking other processes in Cocoa

Invoking other processes is a good way to handle some low-level tasks on the Mac. I'll show you some simple ways to invoke processes and parse their outputs in Cocoa apps as well as some advanced tricks like running a process with administrator privileges.

The Sample Application: Open File Killer

Have you ever tried to empty the Trash, only to be told that some process has one of the files open so Trash can't delete it now?

This week's sample application solves that problem. Drag a file onto Open File Killer and it will tell you all the processes that have the file open and lets you kill them if needed.

openfilekiller.png

How Open File Killer works

This program invokes two other processes to perform its work:

  • lsof - to find the processes that have the specified file open
  • sh - run with administrator privileges to send a SIGKILL to processes
Be careful when using the "Kill process" button — arbitrarily killing system processes can be dangerous to data and system stability so use with caution.

Starting another process with NSTask

You can launch other applications using NSWorkspace's launchApplication: and they will appear as though you launched them from the Finder. To run processes that are not applications, you will typically use NSTask instead.

NSTask does provide the convenience method launchedTaskWithLaunchPath:arguments: to launch another process in one line, however this method does not allow access to the launched task's standard output, which we'll need to get results from lsof.

Reading output from NSTask requires setting up NSPipes and reading from the NSFileHandle for each pipe. Since all this can be a bit verbose, we'll create a reusable method with the following prototype:

+ (NSString *)stringByLaunchingPath:(NSString *)processPath
    withArguments:(NSArray *)arguments
    error:(NSError **)error

This is similar to launchedTaskWithLaunchPath:arguments: but returns the standard out as a string and returns standard error in the userInfo property of the NSError under the key @"standardError".

Excluding the error handling parts, the implementation of this method is as follows:

NSTask *task = [[[NSTask alloc] init] autorelease];

[task setLaunchPath:processPath];
[task setArguments:arguments];
[task setStandardOutput:[NSPipe pipe]];
[task setStandardError:[NSPipe pipe]];

TaskOutputReader *outputReader = [[TaskOutputReader alloc] initWithTask:task];

NSString *outputString = nil;
NSString *errorString = nil;

[outputReader launchTaskAndRunSynchronous];

outputString =
    [[[NSString alloc]
        initWithData:[outputReader standardOutputData]
        encoding:NSUTF8StringEncoding]
    autorelease];
errorString =
    [[[NSString alloc]
        initWithData:[outputReader standardErrorData]
        encoding:NSUTF8StringEncoding]
    autorelease];

[outputReader release];

The TaskOutputReader is a simple class that invokes launch on the task and runs the current NSRunLoop, reading from the standardOut and standardError of the task until the task terminates.

The strangest part to me is the NSPipe part — NSPipe is an opaque class (you can't do anything directly to it) that only works with NSTask and allows you to subsequently invoke fileHandleForReading/fileHandleForWriting. To me, it really seems as though NSPipe shouldn't exist at all and any Unix-level pipes and file descriptors should be created on demand when you invoke standardOutput.

Anyway, that's why I always use a convenience method to wrap it all up. The only reason why you shouldn't use a convenience method like this is if you want progressive output updates (like line-by-line log file output) or two-way communication (command and response) since you can't simply block a return to the calling function these cases.

Parsing text

So we can use the stringByLaunchingPath:withArguments:error: method to launch lsof and pass the path of the file.

The result from lsof looks something like this:

COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
Preview 78180 matt  txt    REG   14,2    52889 1233935 /Users/matt/Downloads/002dfwtw.jpg

We need to parse this result and extract the information that is useful to us: the first two columns of the second row. I know, there are arguments to lsof that will produce more "computer-readable" output but when done right, parsing this output should be easier than reading man-page documentation.

The base Cocoa classes do no really handle sophisticated text parsing on their own. There is no regular expression library by default (get yourself a copy of OgreKit or RegexKit to do that) and while NSString has a few manipulation methods, many of the chop, split, tokenize and line-at-a-time handling functions and operators that are common in scripting languages, do not appear in Cocoa.

You can guess at the reason behind some of these decisions. I would suggest that you're not supposed to store structured data in pure text strings in a language with sophisticated real data structures, so Cocoa doesn't try to make it easier for you.

Another reason would be that C-based languages are more about giving you the basic building blocks to make efficient solutions over any domain, not about giving you already-domain-specific pre-canned solutions. AppKit/UIKit are obvious exceptions where complete, domain-specific solutions are given but in those cases, the solution domain for "application framework" is already fixed (Mac OS X/iPhone), so they are not really narrowing the potential solutions.

The result though, is that any project requiring text handling will likely need to implement a few simple text handling methods. You can get quite practised at using the "basic building blocks" to write small parser methods to handle data that comes from plain text sources like lsof's output.

- (NSArray *)arrayBySeparatingIntoParagraphs
{
    NSUInteger length = [self length];
    NSUInteger paraStart = 0;
    NSUInteger paraEnd = 0;
    NSUInteger contentsEnd = 0;
    NSMutableArray *array = [NSMutableArray array];
    NSRange currentRange;
    while (paraEnd < length)
    {
        [self
            getParagraphStart:¶Start
            end:¶End
            contentsEnd:&contentsEnd
            forRange:NSMakeRange(paraEnd, 0)];
        currentRange = NSMakeRange(paraStart, contentsEnd - paraStart);
        [array addObject:[self substringWithRange:currentRange]];
    }
    return array;
}

You might ask about this function: "Why not just use the existing componentsSeparatedByString: and pass @"\n" as the string?"

The answer is largely because it is recommended that you don't assume \n is the paragraph separator in case the paragraph separator is a Windows line-feed (\r\n) or an old Mac OS 9 carriage return (\r). Okay, irrelevant here but this method can be used anywhere where you need to break an NSString into paragraphs.

Also, if you needed to optimize your code, getParagraphStart:end:contentsEnd:forRange: allows you to parse each line inside the loop, avoiding the need to create the array of lines at all. This is something that the upcoming "blocks" feature of Objective-C will make much easier since you'll be able to pass the block for the inside of the loop as a parameter to the method.

You might also be curious about the difference between a "paragraph" and a "line", especially since the method getLineStart:end:contentsEnd:forRange: also exists. The difference is minor — lines also break on the rarely seen Unicode line-break character (sometimes used to represent HTML's <br>). In this case, it doesn't really matter.

- (NSArray *)tokensSeparatedByCharactersInSet:(NSCharacterSet *)separator
{
    NSScanner *scanner = [NSScanner scannerWithString:self];
    NSMutableArray *array = [NSMutableArray array];
    while (![scanner isAtEnd])
    {
        [scanner scanCharactersFromSet:separator intoString:nil];

        NSString *component;
        if ([scanner scanUpToCharactersFromSet:separator intoString:&component])
        {
            [array addObject:component];
        }
    }
    return array;
}

This method extracts runs of characters not in the separator character set from runs of characters that are. It differs from componentsSeparatedByCharactersInSet: because it treats runs of separator characters as single elements and never outputs empty tokens.

If we pass [NSCharacterSet whitespaceCharacterSet] into this method, it will break the string up by blocks of whitespace, allowing us to extract the columns of text from a row of the lsof output (where the rows are extracted using the arrayBySeparatingIntoParagraphs method).

This method isn't used in the sample app anymore. It didn't handle application names with spaces in them. I've replaced it with an approach that extracts the columns from the lsof output by character index using substringWithRange: and then uses the stringByTrimmingCharactersInSet: method to remove whitespace from the end. I'll leave the method in the post — it is still useful, just not in this case.

In this way we can parse the output of lsof and get the names and process IDs of any process that has a given file path open.

Running with elevated privileges

For killing tasks, I wanted to run with administrator privileges, so that any process could be killed.

To make the design as consistent as possible, I created a method that would invoke a process with different privileges that looked as much like the NSTask method as possible:

+ (NSString *)stringByLaunchingPath:(NSString *)processPath
    withArguments:(NSArray *)arguments
    authorization:(SFAuthorization *)authorization
    error:(NSError **)error

This method then works the same as the previous method except it requires an SFAuthorization object and it will not return standard error in the NSError object.

Creating the authorization object is quite simple:

authorization = [SFAuthorization authorization];
BOOL result =
    [authorization
        obtainWithRights:NULL
        flags:kAuthorizationFlagExtendRights
        environment:NULL
        authorizedRights:NULL
if (!result)
{
    NSLog(@"SFAuthorization error: %@", [error localizedDescription]); 
    return;
}

This is an authorization object for launching another process with administrator rights (kAuthorizationFlagExtendRights) and no other options set. When used, this will prompt the user for an administrator password.

Running the administrator rights process is then handled with:

OSErr processError =
    AuthorizationExecuteWithPrivileges(
        [authorization authorizationRef],
        [processPath UTF8String],
        kAuthorizationFlagDefaults,
        (char *const *)argv,
        &processOutput);

The C-style argv array is a bit annoying to create from an Objective-C NSArray of NSStrings and using fread to read the output from processOutput is also a pain but once the input and output data wrangling is done, this can work as simply as the NSTask-based invocation.

Warning: running with elevated privileges is dangerous. Don't do this casually in your code, think about it first. And as a user of programs, don't enter your administrator password unless you trust the program you're running (be careful of potential malware and trojans).

Other things that make an application

The application in this week's post contains a number of features that aren't relevant to this post but are interesting and useful nevertheless. These include:

  • Drag and drop of files onto the view using draggingEntered:, draggingExited: and performDragOperation: to handle the drag and drawing a highlighting rect to indicate drag focus using NSSetFocusRingStyle and bezierPathWithRect:. See the FileDragReceivingView that I've set as the contentView of the window.
  • Opening files from the Finder/Dock without NSDocument using the NSApplication delegate method application:openFiles: and an "any" DocumentType listed in the Info.plist file.

And oh, how I miss NSArrayController and bindings on NSTableView when I'm working on the iPhone. They make populating a table of results so much easier.

Conclusion

You can download the complete Open File Killer project (70kB).

The default process-launching and string-handling in Cocoa are not heavily geared towards command-line shell style invocation and handling but it doesn't take much to create a few wrapper methods that will make this a lot easier.

Mac OS X has lots of standard in/out based executables: POSIX, BSD, Mac OS X Admin Tools, Developer Tools and any number of open source tools that you might load through package managers or other means. That's a lot of functionality available to your program and, when done right, using these tools should be as simple as invoking a method on a class.

The sample application shows this in action. The code in KillerController is more verbose than an equivalent application in a scripting language might be but verbose compared to another language does not equal worse. The Objective-C code is not significantly more complex than a typical scripting language performing the same tasks (the verbosity is mostly word and operator length plus formatting, not extra operators and commands), yet it is fast, handles errors gracefully and integrates smoothly into the user-interface.