How to handle interactivity with a command line tool from a Cocoa program ?

I am trying to communicate with a command line tool launched as a NSTask from my Cocoa application but I run into various difficulties to get the prompts and other outputs of the tool and to supply user input correctly.
Is there a solution ?
A classic tool used to automate interactive tools under Unix is Expect. However, Expect ships only on Mac OS X 10.3.x (Panther) and is not complete: libexpect is not supplied, and libexpect is what you need to solve this kind of situation (see example below).
To install Expect, you have first to download it from
here.
To compile it, you also need a complete version of Tcl: the one shipping with OS X lacks the private headers required to compile Expect. Don't be afraid: you will not need to link your Cocoa application with the TCl framework to use
libexpect, you just need the sources of Tcl to compile the library.
Create a work directory where you will expand both
expect.tar.gz and the tcl archive.
If you have DarwinPort installed, you could just type
port build tcl in Terminal.app. After the build process completed, you will have to create a symbolic link in the work directory to the tcl distribution in the DarwinPort folder hierarchy.
Something like:

cd work_directory
ln -s path_to_darwing_port/darwinports/dports/lang/tcl/work/tcl8.x.y tcl
If you don't have DarwinPort, just cd to the expanded tcl distribution and use configure and make as usual.
You don't have to install the downloaded
tcl to build libexpect, just to compile it.
Once Tcl is build,
cd to the expanded Expect directory and type:
./configure --with-tcl=../tcl/unix --with-tclinclude=../tcl/generic
make
The tcl in both paths should be the exact name of the folder (or symbolic link) containing the Tcl you just build at previous step.
Once done, you will have a
libexepect5.xy.a library in the Expect directory. (Current version is 5.41)
If you don't want to install the full package with
make install, just create a directory in /usr/local/:
sudo mkdir /usr/local/Expect5.xy
sudo cp libexepect5.xy.a /usr/local/Expect5.xy/
sudo cp expect.h /usr/local/Expect5.xy/
sudo cp expect_comm.h /usr/local/Expect5.xy/
sudo cp libexpect.man /usr/local/man/man1/libexpect.1
sudo ranlib /usr/local/Expect5.xy/libexepect5.xy.a
sudo ln -s /usr/local/Expect5.xy/libexepect5.xy.a /usr/local/Expect5.xy/libexepect.a
Now you are almost done: you just have to add libexepct.a to your Xcode project and /usr/local/Expect5.xy/ to its header search path list.

Here a very basic example of usage with
ftp:
#include
#include
#include
#include "expect.h"

void timedout()
{
fprintf(stderr,"timed outn");
exit(-1);
}

int main()
{
int fd;
FILE *fp ;

exp_loguser = 0; // don't echo to terminal
exp_timeout = 3600; // arbitrary, you may change it for each interaction

if (NULL == (fd = exp_spawnl("/usr/bin/ftp","/usr/bin/ftp","-V","your.ftp.server",NULL))) {
perror("ftp");
exit(-1);
}
if (NULL == (fp = fdopen(fd,"r+"))) // mainly to be able to use printf instead of write...
return(0);
setbuf(fp,(char *)0); // unbuffered IO

if (EXP_TIMEOUT == exp_fexpectl(fp,
exp_regexp,"Name.*:",0,
exp_end)) {
timedout();
}

fprintf(fp,"your_loginn");

if (EXP_TIMEOUT == exp_fexpectl(fp,
exp_glob,"Password:",0,
exp_end)) {
timedout();
}

fprintf(fp,"your_passwordn");
fprintf(fp,"lsn");

int r ;

exp_fexpectl(fp,
exp_glob,"ftp>",0,
exp_end) ;

exp_fexpectl(fp,
exp_glob," ls",0,
exp_end) ;

exp_fexpectl(fp,
exp_glob,"EPSV not understood.",0,
exp_end) ;

do {
r = exp_fexpectl(fp,
exp_glob,"ftp>",0,
exp_glob,"226 Transfer complete.",0,
exp_glob,"*n",1,
exp_end) ;

if (r && strlen(exp_match) && (*exp_match != 'r') && (*exp_match != 'n')) {
printf("%s", exp_match) ;
}
}
while ( r ) ;
printf("n");

// disconnect
fprintf(fp,"%c",0x03);

return 0 ;
}
As you see, the main part of the job will be:
1. to carefully test all the possible results from the tool to correctly define the matching patterns that will make the interactivity really happens,
2. to parse the results to present to the end user.

Expect save you to directly put your hands in the low-level pseudo-terminal technology but doesn't save you to do your home work.


It's been a long time since I did this kind of coding
Authored by: Anonymous on Tuesday, June 29 2004 @ 09:51 PM BST
lacking libexpect, what I would do is use pipes. What I would do is use
popen to create a pair of pipes and use those pipes as stdin and stdout for
the program you are launching. After that you should be able to read the
stdout pipe and write to the stdin pipe.