NSTask trickery for long shell pipelines

I learned a cool trick hack today in the process of trying to help somebody with their cocoa code. Dude was trying to fire an NSTask with some arguments for the current command, and then an additional argument that included the rest of a 3 or 4 stage pipeline. The command in question is /usr/bin/ldapsearch, and he was trying to set the arguments array something like:

arguments = [NSArray arrayWithObjects:
@"-LLL", @"-x", @"-b", @"cn=users,dc=company,dc=com", @"-h", @"dirserver.company.com",
@"cn", @"uid",
@"| /usr/bin/othercommand -with -arguments | /bin/thirdcommand -and -even -more -args",
nil];

This won’t work. Not even close, actually. The proper cocoa way to do this would be to create an NSTask for each command, setting each task’s arguments as necessary. You connect the tasks with NSPipe objects, as explained at cocoadevcentral. Obviously this would become somewhat verbose / tedious if you needed a long pipeline.

Of course, the whole idea of calling shell commands in the first place is usually considered ‘icky’ by most real programmers. The ‘better’ way would be to use the appropriate cocoa / carbon API, and there’s one for just about everything. The thing is that some folks (such as myself) are more comfortable in a unix shell than in Xcode; often it is faster to whip out an NSTask than to learn some Cocoa API to get the job done. Performance geeks will rightly assert that using NSTask is much slower, however the speed difference is not even perceptible to a human unless you’re using NSTask in a rapid-fire fashion (a simple “take input, fire NSTask, collect and display results” is quite fast). Now, I’m certain that I’ll read this again in a few years and laugh at myself for being a hack, but I don’t care :)

For right now, if I’m going to hack, I’m going to hack it good. The trick is this: set the NSTask command to /bin/sh, and the arguments as follows: The first argument is set to “-c”, the second argument is the entire shell pipeline, including as many commands and pipes as you need, and the third argument as the requisite ‘nil’. Basically, we’re now simply passing a little mini shell script directly to /bin/sh. This is roughly akin to creating a small shell script on the fly, writing it to the filesystem, and then executing it with NSTask, except this way there’s no need for any file i/o. Another advantage is that you should (note: untested!) be able to take advantage of other shell functions for which there is no direct equivalent in NSTask land; e.g. angle brackets for input / output redirection, etc.

About dre

I like all kinds of food.
This entry was posted in development. Bookmark the permalink.

3 Responses to NSTask trickery for long shell pipelines

  1. rick says:

    hello there! :-)

    nice post and i’m hoping you can help a bit? i’m interested in this trick but whenever i try it i get no output. using this link as an example:

    http://www.borkware.com/quickies/one?topic=NSTask

    if i run it the standard way (top portion) it’s no problem i get an ouput in NSLog. but with the same code and using this trick you mentioned i get no output in NSLog. funny thing is it seems the command is being run based on the time it takes to complete. maybe using this trick i need to set up a different way to read the output? or is out outdated for leopard? any thoughts hope i’m making sense. :-)

    rick

  2. dre says:

    Hey Rick,

    You say the second method fails, though you are using the ‘same code’… this might not work, as there are differences in the two methods :) Maybe you meant the same shell code… Does the second example on the site work for you as written?

    I’m not sure what you mean about the time duration of the command executed through NSTask… it’s expected that some commands take longer to run than others. Also note that if you try to run a long command on the main thread, the reading of the output pipe will block (causing your GUI to SPOD, if you have a GUI at all that is) until the command is done, if you use readDataToEndOfFile… so you might want to do that on another thread if you want to keep your UI from getting blocked. In this snippet, I’m calling my own readOutput method from the main thread, and passing in an array of pipes that are used for in / out.

    [NSThread detachNewThreadSelector:@selector(readOutput:)
    toTarget:self withObject:pipes];

    More examples, info, and links:
    http://www.cocoadev.com/index.pl?NSTask

    Hope this helps,
    -dre

  3. rick says:

    thanks dre for the comment! :-)

    my problem is actually very basic. i’ll post the code so you can see:

    NSTask *task;
    task = [[NSTask alloc] init];
    [task setLaunchPath: @”/bin/sh”];

    NSArray *arguments;
    arguments =
    [NSArray arrayWithObjects:
    @”-c”, @”ls -l -a -h”, nil];

    [task setArguments: arguments];

    NSPipe *pipe;
    pipe = [NSPipe pipe];
    [task setStandardOutput: pipe];

    NSFileHandle *file;
    file = [pipe fileHandleForReading];

    [task launch];

    NSData *data;
    data = [file readDataToEndOfFile];

    NSString *string;
    string = [[NSString alloc] initWithData: data
    encoding: NSUTF8StringEncoding];
    NSLog (@”woop! got\n%@”, string);

    you see i just took the first example from that link and set the launch path and arguments according to how you described above and seems it should work but for some reason it doesn’t? i don’t get any errors at all, builds fine, and i get the familiar debugger exited with status 0 (foundation tool new project) but no NSLog output? running the standard way is no problem. i’m not an expert with NSTask but usually they’re pretty straightforward. but i’m kinda stuck here? as you mentioned i’m looking to plug in a few more complex terminal commands and it gets a bit difficult the standard way. does this make sense at all why i get no output?

    thanks again!

    rick

Leave a Reply