views:

7389

answers:

6

Hi

How can I execute a terminal command (like grep) from my Objective-C Cocoa application?

Thanks.

+33  A: 

You can use NSTask. Here's an example that would run '/usr/bin/grep foo bar.txt'.

NSTask *task;
task = [[NSTask alloc] init];
[task setLaunchPath: @"/usr/bin/grep"];

NSArray *arguments;
arguments = [NSArray arrayWithObjects: @"foo", @"bar.txt", 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 (@"grep returned:\n%@", string);

NSPipe and NSFileHandle are used to redirect the standard output of the task.

For more detailed information on interacting with the operating system from within your Objective-C application, you can see this document on Apple's Development Center: Interacting with the Operating System.

Edit: Included fix for NSLog problem

If you are using NSTask to run a command-line utility via bash, then you need to include this magic line to keep NSLog working:

//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

In context:

NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

An explanation is here: http://www.cocoadev.com/index.pl?NSTask

Gordon Wilson
Thanks a lot. If I need to add any options like -e, do I add them to the arguments array as well?
lostInTransit
Yup, 'arguments = [NSArray arrayWithObjects: @"-e", @"foo", @"bar.txt", nil];'
Gordon Wilson
Just don't forget to terminate the arrayWithObjects call with a nil :)
Jason Coco
The apple dev docs are a nice addition, Jason. Thanks.
Gordon Wilson
+3  A: 

Or since Objective C is just C with some OO layer on top you can use the posix conterparts:

int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0);
int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0);
int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

They are included from unistd.h header file.

Paulo Lopes
+5  A: 

fork, exec, and wait should work, if you're not really looking for a Objective-C specific way. fork creates a copy of the currently running program, exec replaces the currently running program with a new one, and wait waits for the subprocess to exit. For example (without any error checking):

pid_t p = fork();
if (p == 0) {
    /* fork returns 0 in the child process. */
    execl("/other/program/to/run", "/other/program/to/run", "foo", NULL);
} else {
    /* fork returns the child's PID in the parent. */
    int status;
    wait(&status);
    /* The child has exited, and status contains the way it exited. */
}

/* The child has run and exited by the time execution gets to here. */

There's also system, which runs the command as if you typed it from the shell's command line. It's simpler, but you have less control over the situation.

I'm assuming you're working on a Mac application, so the links are to Apple's documentation for these functions, but they're all POSIX, so you should be to use them on any POSIX-compliant system.

Zach Hirsch
+3  A: 

There is also good old POSIX system("echo -en '\007'");

nes1983
DO NOT RUN THIS COMMAND. (In case you do not know what this command does)
Justin
Changed it to something slightly safer … (it beeps)
nes1983
+4  A: 

in the spirit of sharing... this is a method I use frequently to run shell scripts. you can add a script to your product bundle (in the copy phase of the build) and then have the script be read and run at runtime. note: this code looks for the script in the privateFrameworks sub-path. warning: this could be a security risk for deployed products, but for our in-house development it is an easy way to customize simple things (like which host to rsync to...) without re-compiling the application, but just editing the shell script in the bundle.

//------------------------------------------------------
-(void) runScript:(NSString*)scriptName
{
    NSTask *task;
    task = [[NSTask alloc] init];
    [task setLaunchPath: @"/bin/sh"];

    NSArray *arguments;
    NSString* newpath = [NSString stringWithFormat:@"%@/%@",[[NSBundle mainBundle] privateFrameworksPath], scriptName];
    NSLog(@"shell script path: %@",newpath);
    arguments = [NSArray arrayWithObjects:newpath, 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 (@"script returned:\n%@", string);    
}
//------------------------------------------------------

Edit: Included fix for NSLog problem

If you are using NSTask to run a command-line utility via bash, then you need to include this magic line to keep NSLog working:

//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

In context:

NSPipe *pipe;
pipe = [NSPipe pipe];
[task setStandardOutput: pipe];
//The magic line that keeps your log where it belongs
[task setStandardInput:[NSPipe pipe]];

An explanation is here: http://www.cocoadev.com/index.pl?NSTask

kent
A: 

I'm surprised no one really got in to blocking/non-blocking call issues :-/

Custos Mortem
Feel free to edit your answer (or ask a question, if you mean to ask about them) to cover those.
Peter Hosey