I want to implement pipe command in shell, such as ls | cat | wc
. Here's my pipe command implementation.
I use a pipe to store the results generated by every command. For every child process, it:
execvp
to execute the command with the inputHere's my code:
while(getline(std::cin, s)) {
if(s == "exit") {
return EXIT_SUCCESS;
}
// initialize a pipe
int fd[2];
if(pipe(fd) == -1) {
perror("pipe creation failed!");
return EXIT_FAILURE;
}
// split the command into multiple parts
vector<string> tokens;
boost::algorithm::split(tokens, s, boost::is_any_of("|"),boost::token_compress_on);
for(auto& command: tokens) {
// prepare to run the current command
// get the current command
boost::algorithm::trim(command);
// split the command into an array of args
vector<string> args;
boost::algorithm::split(args,command,boost::is_any_of(" "),boost::token_compress_on);
int argc = args.size();
if(argc < 1) {
cerr << "We need a command!" << endl;
return EXIT_FAILURE;
}
// run the current command
pid_t child = fork();
if(child == 0) {
// setup the file name and input arguments
const char* filename = args[0].c_str();
char** argv = new char*[argc + 1];
for(int i = 0; i < argc; i++) {
string args_str = args[i];
argv[i] = new char[10];
strcpy(argv[i],args_str.c_str());
}
argv[argc] = nullptr;
// write the pipe value into stdin
dup2(fd[0], STDIN_FILENO);
close(fd[0]);
// write stdout to the pipe
dup2(fd[1], STDOUT_FILENO);
close(fd[1]);
// use execvp() to run the commmand
execvp(filename,argv);
// exec didn't work, so an error must have been occurred
cerr << strerror(errno) << endl;
delete[] argv;
return EXIT_FAILURE;
}
// wait for the child process to complete
int status;
waitpid(child,&status,0);
}
// read out the pipe
char* buffer = new char[BUF_SIZ];
int count = read(fd[0],buffer,BUF_SIZ);
if(count > 0) {
fprintf(stdout, "%s", buffer);
}
delete buffer;
close(fd[0]);
close(fd[1]);
cout << "$ ";
}
When I test the program by entering ls | cat
. The last command successfully put the result of ls
into the pipe. But I could not redirect input from pipe to STDIN_FILENO when executing the cat
command.
The program just blocks when executing ls | cat
. After debugging, I found that it's because the cat
command is still waiting for user to input something, and the parent process is waiting for the child process to terminate.
I'm the one who raised this question. Now I've worked out the problem now and successfully implemented the multi-pipe program. I made 3 changes to the previous code:
pipe -> STDIN -> execvp -> STDOUT -> pipe
. But if I only use 1 pipe, the STDIN
and STDOUT
are connected. So In the current version of my program, I create a new pipe before fork()
and close its write end, but I record its read end in a variable fd_in
.fd_in
is set to 0 at first. Because some commands such as cat
need to obtain input from the STDIN
instead of pipe. After each command execution, the fd_in
is updated to the current pipe's read end for the next command execution. While working with this problem, I found that pipe is not the same as local variable, which is pushed on the stack on process memory. It's stored in kernel memory rather than the process memory, so the created pipe won't go away immediately after the loop exits. But if we declares a local variable in a loop, it's gone after the loop exits. If we record the read end of the pipe, we could still reach it in the next loop.count
and compare it to the length of command vector in order to determine if it is the last command to execute. If it is not the last command, write the result to the pipe. Otherwise, just print out the result to the console. Therefore, there's no need to use the system call read
to get the result out of pipe and store it in a buffer in the parent process. It's mainly for those command which print out results to the console immediately, such as cat
. Besieds, it simplify the design.The new version of code as follows:
while (getline(std::cin, s))
{
if (s == "exit")
{
return EXIT_SUCCESS;
}
int fd[2];
int in_fd = 0; // input fd
// split the command into multiple parts
vector<string> tokens;
boost::algorithm::split(tokens, s, boost::is_any_of("|"), boost::token_compress_on);
int count = 1;
int command_num = tokens.size();
for (auto &command : tokens)
{
// initialize a pipe
if (pipe(fd) == -1)
{
perror("pipe creation failed!");
return EXIT_FAILURE;
}
// prepare to run the current command
// get the current command
boost::algorithm::trim(command);
// split the command into an array of args
vector<string> args;
boost::algorithm::split(args, command, boost::is_any_of(" "), boost::token_compress_on);
int argc = args.size();
if (argc < 1)
{
cerr << "We need a command!" << endl;
return EXIT_FAILURE;
}
// run the current command
pid_t child = fork();
if (child == 0)
{
// setup the file name and input arguments
const char *filename = args[0].c_str();
char **argv = new char *[argc + 1];
for (int i = 0; i < argc; i++)
{
string args_str = args[i];
argv[i] = new char[10];
strcpy(argv[i], args_str.c_str());
}
argv[argc] = nullptr;
if (in_fd != 0)
{
// write the pipe value into stdin
dup2(in_fd, STDIN_FILENO);
close(in_fd);
}
if (count != command_num)
{
// write stdout to the pipe
dup2(fd[1], STDOUT_FILENO);
close(fd[1]);
}
// use execvp() to run the commmand
execvp(filename, argv);
// exec didn't work, so an error must have been occurred
cerr << strerror(errno) << endl;
delete[] argv;
return EXIT_FAILURE;
}
// wait for the child process to complete
int status;
waitpid(child, &status, 0);
// close the current pipe write fd
close(fd[1]);
in_fd = fd[0];
count += 1;
}
// // read out the pipe
// char buffer[BUF_SIZ];
// int count = read(in_fd, buffer, BUF_SIZ);
// buffer[count] = '\0';
// if (count > 0)
// {
// fprintf(stdout, "%s", buffer);
// }
close(in_fd);
cout << "$ ";
}
Thanks for Some programmmer dude's help. And I refer to an answer from this question in the process of solving this question. I hope my implementation will give you some inspirations.