2

I want to use NSTask to simulate the Terminal to run commands. The codes as follows. It can get input in loop and return the process output.

int main(int argc, const char * argv[])
{
  @autoreleasepool {      
    while (1) {
        char str[80] = {0};
        scanf("%s", str);
        NSString *cmdstr = [NSString stringWithUTF8String:str];

        NSTask *task = [NSTask new];
        [task setLaunchPath:@"/bin/sh"];
        [task setArguments:[NSArray arrayWithObjects:@"-c", cmdstr, nil]];

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

        [task launch];

        NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];

        [task waitUntilExit];

        NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@", string);

    }
}

My question is: when a loop is end, the running environment restore to the initialization state. For example, the default running path is /Users/apple, and I run cd / to change the path to /, and then run pwd, it return the /Users/apple rather than the /.

So how can I use NSTask to simulate the Terminal completely ?

Tom Jacky
  • 203
  • 1
  • 7
  • 20

1 Answers1

7

cd and pwd are shell built-in commands. If you execute the task

/bin/sh -c "cd /"

there is no way of getting the changed working directory back to the calling process. The same problem exists if you want to set variables MYVAR=myvalue.

You could try to parse these lines separately and update the environment. But what about multi-line commands like

for file in *.txt
do
    echo $file
done

You cannot emulate that by sending each line to separate NSTask processes.

The only thing you could do is to start a single /bin/sh process with NSTask, and feed all the input lines to the standard input of that process. But then you can not use readDataToEndOfFile to read the output, but you have to read asynchronously (using [[pipe fileHandleForReading] waitForDataInBackgroundAndNotify]).

So in short: you can simulate the Terminal only by running a (single) shell.

ADDED: Perhaps you can use the following as a starting point for your app. (I have omitted all error checking.)

int main(int argc, const char * argv[])
{
    @autoreleasepool {

        // Commands are read from standard input:
        NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput];

        NSPipe *inPipe = [NSPipe new]; // pipe for shell input
        NSPipe *outPipe = [NSPipe new]; // pipe for shell output

        NSTask *task = [NSTask new];
        [task setLaunchPath:@"/bin/sh"];
        [task setStandardInput:inPipe];
        [task setStandardOutput:outPipe];
        [task launch];

        // Wait for standard input ...
        [input waitForDataInBackgroundAndNotify];
        // ... and wait for shell output.
        [[outPipe fileHandleForReading] waitForDataInBackgroundAndNotify];

        // Wait asynchronously for standard input.
        // The block is executed as soon as some data is available on standard input.
        [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
                                                          object:input queue:nil
                                                      usingBlock:^(NSNotification *note)
         {
             NSData *inData = [input availableData];
             if ([inData length] == 0) {
                 // EOF on standard input.
                 [[inPipe fileHandleForWriting] closeFile];
             } else {
                 // Read from standard input and write to shell input pipe.
                 [[inPipe fileHandleForWriting] writeData:inData];

                 // Continue waiting for standard input.
                 [input waitForDataInBackgroundAndNotify];
             }
         }];

        // Wait asynchronously for shell output.
        // The block is executed as soon as some data is available on the shell output pipe. 
        [[NSNotificationCenter defaultCenter] addObserverForName:NSFileHandleDataAvailableNotification
                                                          object:[outPipe fileHandleForReading] queue:nil
                                                      usingBlock:^(NSNotification *note)
         {
             // Read from shell output
             NSData *outData = [[outPipe fileHandleForReading] availableData];
             NSString *outStr = [[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding];
             NSLog(@"output: %@", outStr);

             // Continue waiting for shell output.
             [[outPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
         }];

        [task waitUntilExit];

    }
    return 0;
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks, Martin. But how can I start a single shell with `NSTask`? Can you provider some code sample? – Tom Jacky Nov 04 '12 at 12:36
  • @TomJacky: You would create and launch the task just once, and use a pipe for the standard input as you did for standard output. - Perhaps you can be a bit more specific what you actually want to do. Is it really only a command line program that reads commands from the standard input and executes the commands? – Martin R Nov 04 '12 at 12:53
  • Yes, it is only a command line program to do that. In short, what I wana to do is emulate a Terminal shell. I do not know how to read input commands in loop and give these command to the pipe of std. input and read outputs from the pipe of std. output. Are there any example codes on then net? – Tom Jacky Nov 04 '12 at 13:05
  • @TomJacky: What do you mean exactly with "emulate a Terminal"? Where is the difference between your program and /bin/sh or /bin/bash ? – Martin R Nov 04 '12 at 13:21
  • Sorry, I did not explain enough. Exactly, I'm using the the /bin/sh. The meaing of "emulate a Terminal" is that I want to input a command to /bin/sh and get the output, and then send another command to it and get another output, and that cycle repeats, just like directly interacting with the sh or bash. What i don't know is that how can I use a single NSTask process to handle the loop commands input and their output? – Tom Jacky Nov 04 '12 at 13:46
  • @TomJacky: And where do the commands come from? User input? - The problem is that one input line can generate zero, one or multiple output lines. So you cannot just pipe one input line into the shell task and read the output, because you don't know how much output to expect. Therefore both reading the user input and reading the shell output must be done asynchronously. This is not too complicated but more than a few lines of code. You have to use notifications to check if input/output is available. Is this really what you want to do? – Martin R Nov 04 '12 at 14:38
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/19045/discussion-between-martin-r-and-tom-jacky) – Martin R Nov 04 '12 at 14:38
  • @TomJacky: I have added a complete code sample to my answer. Please let me know if that helps. – Martin R Nov 05 '12 at 06:26
  • Wonderful job! This is what I need, thank you very much. But I have still a little puzzle with the code sample, maybe I am newbie about objc, especially about `Notification` mechanism. That is, if I instead the statement `NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput]` with `[input writeData:aData]`, where `aData` is NSData value, why no notification post to Notification Center? – Tom Jacky Nov 05 '12 at 07:27
  • @TomJacky: To write directly to the shell input pipe, use `[[inPipe fileHandleForWriting] writeData:aData]`. – Martin R Nov 05 '12 at 10:29
  • `@MartinR:` With `[[inPipe fileHandleForWriting] writeData:aData`, how to trigger the Notification? – Tom Jacky Nov 05 '12 at 13:52
  • @TomJacky: Answer in http://chat.stackoverflow.com/rooms/19045/discussion-between-martin-r-and-tom-jacky – Martin R Nov 05 '12 at 21:24