C++ shell with forks and pipes

As an assignment for my operating systems class, we were to write a shell in C or C++. I'm putting my work here under the GNU General Public License v3 in hopes that it will be helpful for someone else, presumably some future student arguing with the C language, which I find infinitely frustrating to work with sometimes.

My shell is written in 3 files: general.h, main.cpp, and functions.cpp. It uses a Makefile to compile everything. The main point of the assignment was for us to make use of execvp(), execlp(), fork(), dup2(), and waitpid() to use pipes and forks.

It's a very limited shell in that it can handle these types of commands:

  • cat file1 >> file2
  • ps ax | grep foo
  • less file1

Hence, you can do file redirects, pipes, and regular commands. We were not allowed to use the system() function anywhere in our code.

general.h

C++

/*
    Copyright 2008 Sarah Vessels

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <iostream>
#include <fstream>
#include <sys/stat.h>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

// Will be used to create an array to hold individual arguments passed by
// the user on the command line.
const int MAX_ARGS = 256;

enum PipeRedirect {PIPE, REDIRECT, NEITHER};

// Splits a user's command into two commands, or a command and a file name.
PipeRedirect parse_command(int, char**, char**, char**);

// Pipes the first command's output into the second.
void pipe_cmd(char**, char**);

// Reads input from the user into the given array and returns the number of
// arguments taken in.
int read_args(char**);

// Redirects the output from the given command into the given file.
void redirect_cmd(char**, char**);

// Given the number of arguments and an array of arguments, this will execute
// those arguments.  The first argument in the array should be a command.
void run_cmd(int, char**);

// Given a string of user input, this will determine if the user wants to
// quit the shell.
bool want_to_quit(string);

main.cpp

C++

/*
    Copyright 2008 Sarah Vessels

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "general.h"

// Takes user input until they quit the shell, and passes that input as
// arguments to be run.
int main() {
  char *argv[MAX_ARGS], *cmd1[MAX_ARGS], *cmd2[MAX_ARGS];
  PipeRedirect pipe_redirect;
  int argc;

  // Keep returning the user to the prompt ad infinitum unless they enter
  // 'quit' or 'exit' (without quotes).
  while (true) {
    // Display a prompt.
    cout << "SarahShell> ";

    // Read in a command from the user.
    argc = read_args(argv);

    // Decipher the command we just read in and split it, if necessary, into
    // cmd1 and cmd2 arrays.  Set pipe_redirect to a PipeRedirect enum value to
    // indicate whether the given command pipes, redirects, or does neither.
    pipe_redirect = parse_command(argc, argv, cmd1, cmd2);

    // Determine how to handle the user's command(s).
    if (pipe_redirect == PIPE)          // piping
      pipe_cmd(cmd1, cmd2);
    else if (pipe_redirect == REDIRECT) // redirecting
      redirect_cmd(cmd1, cmd2);
    else
      run_cmd(argc, argv);              // neither

    // Reset the argv array for next time.
    for (int i=0; i<argc; i++)
      argv[i] = NULL;
  }

  // Let the OS know everything is a-okay.
  return 0;
}

functions.cpp

C++

/*
    Copyright 2008 Sarah Vessels

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "general.h"

// Given the number of arguments (argc) in an array of arguments (argv), this
// will go through those arguments and, if necessary, bifurcate the arguments
// into arrays cmd1 and cmd2.  It will return a PipeRedirect enum representing
// whether there was a pipe in the command, a redirect to a file, or neither.
// cmd1 and cmd2 will only be populated if there was a pipe or a redirect.
PipeRedirect parse_command(int argc, char** argv, char** cmd1, char** cmd2) {
  // Assume no pipe or redirect will be found.
  PipeRedirect result = NEITHER;

  // Will hold the index of argv where the pipe or redirect is found.
  int split = -1;

  // Go through the array of arguments...
  for (int i=0; i<argc; i++) {
    // Pipe found!
    if (strcmp(argv[i], "|") == 0) {
      result = PIPE;
      split = i;

    // Redirect found!
    } else if (strcmp(argv[i], ">>") == 0) {
      result = REDIRECT;
      split = i;
    }
  }

  // If either a pipe or a redirect was found...
  if (result != NEITHER) {
    // Go through the array of arguments up to the point where the
    // pipe/redirect was found and store each of those arguments in cmd1.
    for (int i=0; i<split; i++)
      cmd1[i] = argv[i];

    // Go through the array of arguments from the point where the pipe/redirect
    // was found through the end of the array of arguments and store each
    // argument in cmd2.
    int count = 0;
    for (int i=split+1; i<argc; i++) {
      cmd2[count] = argv[i];
      count++;
    }

    // Terminate cmd1 and cmd2 with NULL, so that execvp likes them.
    cmd1[split] = NULL;
    cmd2[count] = NULL;
  }

  // Return an enum showing whether a pipe, redirect, or neither was found.
  return result;
}

// This pipes the output of cmd1 into cmd2.
void pipe_cmd(char** cmd1, char** cmd2) {
  int fds[2]; // file descriptors
  pipe(fds);
  pid_t pid;

  // child process #1
  if (fork() == 0) {
    // Reassign stdin to fds[0] end of pipe.
    dup2(fds[0], 0);

    // Not going to write in this child process, so we can close this end
    // of the pipe.
    close(fds[1]);

    // Execute the second command.
    execvp(cmd2[0], cmd2);
    perror("execvp failed");

  // child process #2
  } else if ((pid = fork()) == 0) {
    // Reassign stdout to fds[1] end of pipe.
    dup2(fds[1], 1);

    // Not going to read in this child process, so we can close this end
    // of the pipe.
    close(fds[0]);

    // Execute the first command.
    execvp(cmd1[0], cmd1);
    perror("execvp failed");

  // parent process
  } else
    waitpid(pid, NULL, 0);
}

// This will get input from the user, split the input into arguments, insert
// those arguments into the given array, and return the number of arguments as
// an integer.
int read_args(char **argv) {
  char *cstr;
  string arg;
  int argc = 0;

  // Read in arguments till the user hits enter
  while (cin >> arg) {
    // Let the user exit out if their input suggests they want to.
    if (want_to_quit(arg)) {
      cout << "Goodbye!\n";
      exit(0);
    }

    // Convert that std::string into a C string.
    cstr = new char[arg.size()+1];
    strcpy(cstr, arg.c_str());
    argv[argc] = cstr;

    // Increment our counter of where we're at in the array of arguments.
    argc++;

    // If the user hit enter, stop reading input.
    if (cin.get() == '\n')
      break;
  }

  // Have to have the last argument be NULL so that execvp works.
  argv[argc] = NULL;
 
  // Return the number of arguments we got.
  return argc;
}

void redirect_cmd(char** cmd, char** file) {
  int fds[2]; // file descriptors
  int count;  // used for reading from stdout
  int fd;     // single file descriptor
  char c;     // used for writing and reading a character at a time
  pid_t pid;  // will hold process ID; used with fork()

  pipe(fds);

  // child process #1
  if (fork() == 0) {
    // Thanks to http://linux.die.net/man/2/open for showing which headers
    // need to be included to use this function and its flags.
    fd = open(file[0], O_RDWR | O_CREAT, 0666);

    // open() returns a -1 if an error occurred
    if (fd < 0) {
      printf("Error: %s\n", strerror(errno));
      return;
    }

    dup2(fds[0], 0);

    // Don't need stdout end of pipe.
    close(fds[1]);

    // Read from stdout...
    while ((count = read(0, &c, 1)) > 0)
      write(fd, &c, 1); // Write to file.

    // Okay, so this is a bit contrived, but when I didn't have any kind of exec
    // function call here, I got my SarahShell prompt repeated over and over
    // again on the Multilab machines, I think because of this crazy child
    // process or something.  When I put this execlp here with the useless call
    // to echo, however, that looping stops and you can actually enter things
    // at the prompt again, hurray!
    execlp("echo", "echo", NULL);

  // child process #2
  } else if ((pid = fork()) == 0) {
    dup2(fds[1], 1);

    // Don't need stdin end of pipe.
    close(fds[0]);

    // Output contents of the given file to stdout.
    execvp(cmd[0], cmd);
    perror("execvp failed");

  // parent process
  } else {
    waitpid(pid, NULL, 0);
    close(fds[0]);
    close(fds[1]);
  }
}

// Given the number of arguments (argc) and an array of arguments (argv),
// this will fork a new process and run those arguments.
// Thanks to http://tldp.org/LDP/lpg/node11.html for their tutorial on pipes
// in C, which allowed me to handle user input with ampersands.
void run_cmd(int argc, char** argv) {
  pid_t pid;
  const char *amp;
  amp = "&";
  bool found_amp = false;
 
  // If we find an ampersand as the last argument, set a flag.
  if (strcmp(argv[argc-1], amp) == 0)
    found_amp = true;

  // Fork our process
  pid = fork();
 
  // error
  if (pid < 0)
    perror("Error (pid < 0)");
 
  // child process
  else if (pid == 0) {
    // If the last argument is an ampersand, that's a special flag that we
    // don't want to pass on as one of the arguments.  Catch it and remove
    // it here.
    if (found_amp) {
      argv[argc-1] = NULL;
      argc--;
    }

    execvp(argv[0], argv);
    perror("execvp error");

  // parent process
  } else if (!found_amp)
    waitpid(pid, NULL, 0); // only wait if no ampersand
}

// Given a string of user input, this determines whether or not the user
// wants to exit the shell.
bool want_to_quit(string choice) {
  // Lowercase the user input
  for (unsigned int i=0; i<choice.length(); i++)
    choice[i] = tolower(choice[i]);

  return (choice == "quit" || choice == "exit");
}

Makefile

snazzy_shell: main.o functions.o
	g++ -o snazzy_shell main.o functions.o; rm *.o
functions.o: functions.cpp general.h
	g++ -Wall -c functions.cpp
main.o: main.cpp general.h
	g++ -Wall -c main.cpp
This entry was posted in Daily life and tagged , by Sarah. Bookmark the permalink.

10 thoughts on “C++ shell with forks and pipes

  1. Thats very nice of you to release this under GPL.

    This will undoubtedly be useful to many. A common program which runs under both windows and unix will also be of much value. :)

  2. Not only is it awesome you’re releasing this, but including tons of comments! Thanks a lot :)

    I plan to take an OS course at some point (CS degree ftw) and I’ll bookmark this for then :P

    I really like the “SarahShell>” haha :)

  3. I tested the program it works well, but there is some problem when you use pipe. The program will crash if you try use pipe. I tried this cmd “ls-ali | more” and it will show this message “execvp error: No such file or directoy” at the end. After that it will hanged.

  4. Check this working pipe:

    void pipe_cmd(char** cmd1, char** cmd2){
               int fds[2];
               pipe(fds);
    	   pid_t pid1, pid2;
    
               // child process #1
               if (pid1 = fork() == 0) {
                    dup2(fds[1], 1);
    		close(fds[0]);
    		close(fds[1]);
                    execvp(cmd1[0], cmd1);
                    perror("execvp failed");
    
                    // child process #2
                    } else if ((pid2 = fork()) == 0) {
                      dup2(fds[0], 0);
    
                      close(fds[1]);
                      execvp(cmd2[0], cmd2);
                      perror("execvp failed");
    
                } else {
        	close(fds[1]);
    
        	waitpid(pid1, NULL, 0);
    	waitpid(pid2, NULL, 0);
    
    		}
    }
  5. Hi, I also have had to write a shell in C++ for my OS class… mine is even more complex, though, because we had to support an arbitrarily high number of pipes, file input, and file output all at the same time… :-/

    Thanks for posting your code, it was quite helpful in figuring out what my shell was doing wrong (piping is the bane of my existence…)

    Just a note: the reason you have the infinite prompt loop in “redirect_cmd” is because the child process is an *exact* copy of the parent process, including the program counter, the execution stack and your “main”. So the child was continuing on past the end of the if block, forking another child process, returning to main, printing a prompt, and so on. The reason the call to “exec” fixed it is process overlaying, where the execution code of the process is replaced with the execution code of the exec’d function. Thus, because “echo” calls exit, your process obeyed and exited.

    A better solution would be to simply call exit(0) there instead of exec.

  6. Hi,
    This is such a useful tutorial. I have just finished writing my shell (almost). I am having issues with redirecting input. I was searching online and came across this page. Great blog, great info and useful info. Good work!:)

    Anna

  7. Again thanks tons to sarah for posting her code which came a long way in helping me in my shell project.

    heres my version of the shell which handles multiple piping, in_redirects, and out_redirects

    thanks again sarah!

    main:

    //import header file
    #include “system_call.h”

    using namespace std;

    int main() {

    //initialize three char array: one for original commands, and the next
    //two if if a pipe function is used with first containing the commands
    //in the correct order and the next contianing exactly which pipe type(|, )
    char *cmds[MAX_ARGS], *ordered_cmds[MAX_ARGS], *cmd_types[MAX_ARGS];;

    //initialize enum pipe_redirect defined in the header file
    PipeRedirect pipe_redirect;

    //while “C++_Shell” prompt
    while (true) {

    cout < “;
    //immediately call parse_command function and return whther any pipes will be needed
    pipe_redirect = parse_command(cmds);
    //if pipes needed, call cmds_prep funtion to prepare two arrays for run_pipelines function
    if (pipe_redirect == PIPE_REDIRECT)
    cmds_prep(cmds, ordered_cmds, cmd_types);
    //else execute commands with no pipes
    else
    exec_cmd(cmds);

    //clear all respective arrays
    for (int i=0; i<MAX_ARGS; i++)
    {
    ordered_cmds[i] = NULL;
    cmds[i] = NULL;
    cmd_types[i] = NULL;
    }

    }

    //let OS know evrything is
    return 0;
    }

    header file:

    //include all librarys needed for system calls, string functions, and stream
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include

    using namespace std;

    //initialize enum piperedirect which can be either PIPE_REDIRECT indicating piping will be used
    //or NEITHER
    enum PipeRedirect {PIPE_REDIRECT, NEITHER};

    //initialize constant integer at 256 to be used for most array limits and in most loops
    const int MAX_ARGS = 256;

    //initialize amp_flag and set to false
    static bool amp_flag = false;

    //this function reads in user commands ‘live’ and checks if user wants to quit
    //if not, takes user input and converts it to char array compatible format(c-string)
    //and prepares original commands array. Finally, checks if any types of pipe commands were entered
    // and returns enum piperedirect as such
    PipeRedirect parse_command(char**);

    //executes commands without pipes and is instructed to wait if the amp_flag is still false
    void exec_cmd(char**);

    //prepares commands for run_pipelines function which requires the commands be in a set order
    //depending on which specific pipe command is used. Also sets seperate array with pipe types
    //and prepares integer for number of times run_piplines loop must iterate
    void cmds_prep(char**, char**, char**);

    //most complex function. Iterates through a loop and executes command depending on pipe
    //type accordingly. Also handles pipe maintnenance(two pipes used) through each iteration
    //accordingly by closing and duplicating relevant pipes
    void run_pipelines(int, char**, char**);

    //simple bool function which checks if the user entered the words ‘quit’ or ‘exit’
    bool check_quit(string);

    bool checkfor_amp(string);

    class which handles everything:
    #include “system_call.h”

    PipeRedirect parse_command(char** cmds) {

    // Assume no pipe or redirect will be found.
    PipeRedirect result = NEITHER;

    //initiate character used as the ‘c-string’
    char *cstr;

    //initiate string which will be read in as user arguments
    string str_cmd;

    //initiate integer to count number of arguments
    int num_of_args = 0;

    //while user is entering input
    while(cin >> str_cmd)
    {

    //check if user wants to quit
    if (check_quit(str_cmd))
    {
    //if true, print message and exit
    cout << "Till next time\n";
    exit(0);
    }

    //check for ampersand
    if (checkfor_amp(str_cmd))
    {
    amp_flag = true;
    break;
    }

    //define size of 'c-string', copy original argument into 'c-string',
    //and put 'c-string' into current location in array
    cstr = new char[str_cmd.size()+1];
    strcpy(cstr, str_cmd.c_str());
    cmds[num_of_args] = cstr;

    num_of_args++;

    //if user hits return
    if (cin.get() == '\n')
    break;

    }

    //loop through arguments and check for any typr of pipe command
    for (int i=0; i”) == 0 || strcmp(cmds[i], “<") == 0)
    {
    // Pipe found!
    result = PIPE_REDIRECT;
    break;
    }

    }

    //make last element in array NULL for execvp call
    cmds[num_of_args] = NULL;

    //return piperedirect
    return result;
    }

    void exec_cmd(char** cmds)
    {

    //initiate integer for pid
    pid_t pid;

    //fork process
    pid = fork();

    //check for error in pid returned
    if (pid < 0)
    perror("Error (pid < 0)");

    // child process
    else if (pid == 0)
    {

    //execute command and return error if execvp fails
    execvp(cmds[0], cmds);
    perror("execvp error");

    }

    // parent process
    else if(!amp_flag)
    // only wait if no ampersand
    waitpid(pid, NULL, 0);

    }

    void cmds_prep(char** cmds,char** ordered_cmds,char** cmd_types)
    {
    //initiate two integers, first for index(type_count) in cmd_types array, and second for number of commands
    int type_count = -1, num_cmds = 0;

    //initiate character which will be all user arguments(including pipe arguments) concatenated
    char *cmds_cat_final;

    //initiate string which will be used to concatenate arguments
    string cmds_cat = "";

    //loop through arguments and concatenate
    for(int i = 0; i < MAX_ARGS;i++)
    {
    if(cmds[i] != NULL)
    {
    cmds_cat += cmds[i];
    cmds_cat += " ";
    }

    //hit NULL so break out of loop
    else
    break;

    }

    //define size of final concatenated string, copy original string into final concatenated char,
    //and put result into first location in orginal commands array since this array will no longer
    //be used
    cmds_cat_final = new char[cmds_cat.size()+1];
    strcpy(cmds_cat_final, cmds_cat.c_str());
    cmds[0] = cmds_cat_final;

    //loop through commands array to fill pipe type array
    for(int i = 0; i < MAX_ARGS; i++)
    {
    //done, so break
    if(cmds[i] == NULL)
    break;

    //found pipe command
    else if (strcmp(cmds[i], "”) == 0)
    cmd_types[++type_count] = cmds [i];

    }

    //loop through pipe types array and order commands in the ordered_commands array accordingly
    //as well as count number of ordered_cmds elements
    for(int i = 0; i “) == 0)
    {
    //seperate preceding section from pipe-type syntax and put in array
    ordered_cmds[i] = strsep(&cmds[0], cmd_types[i]);
    num_cmds++;
    }

    //found an in_redirect which is a little more tricky since the order of
    //arguments before and after the “<" command need to be reversed so that
    //the name of the text file is put first so that the file contents can be
    //written to the pipe in the run_pipelines function
    else if (strcmp(cmd_types[i], "<") == 0)
    {
    //put section of arguments before the redirect command in the next
    //element of the ordered_cmds array
    ordered_cmds[i + 1] = strsep(&cmds[0], cmd_types[i]);
    num_cmds++;

    //if there is another pipe-type command, then seperate the section
    //after the "<" command and put it in the current element of the
    //ordered_cmds array
    if(cmd_types[i + 1] != NULL)
    {
    ordered_cmds[i] = strsep(&cmds[0], cmd_types[i + 1]);
    num_cmds++;
    }

    //else just put the rest of the arguments in current section
    //since all other arguments have already been seperated and taken out
    else
    {
    ordered_cmds[i] = cmds[0];
    cmds[0] = NULL;
    }

    //since handled two sections of commands, increment i
    i++;
    }
    }

    //call run_pipelines with the number of command sections,
    //prepared array of arguments in correct order, and array of pipe-types
    run_pipelines(num_cmds, ordered_cmds, cmd_types);

    }

    void run_pipelines(int num_cmds, char** ordered_cmds,char** cmd_types)
    {
    //initiate two chars, one used to read and write from a file, and another to hold
    //a single argument seperated by " " temporarely
    char buffer, *temp_cmd;

    //initiate two integer arrays, one for the old pipe, and one for the new pipe
    //and intiate two integers, one for the file reader, and one to count characters
    //written or ead
    int old_fds[2], new_fds[2], file, char_count;

    //initiate integer for pid
    pid_t pid;

    //loop through argument sections seperately and each time fork of
    //a child process to execute relative command section
    for(int i = 0; i 0)
    temp_cmds[++cmd_count] = temp_cmd;

    }

    } while(strlen(ordered_cmds[i]) > 0);

    //prepare last element as NULL for execvp function
    temp_cmds[++cmd_count] = NULL;

    //if there is a next commands section, make new pipe
    if( i 0)
    {
    close(old_fds[1]);
    dup2(old_fds[0], 0);
    close(old_fds[0]);

    }

    //if their is a next commands section, close new pipe and
    //duplicate write part of new pipe
    if( i < num_cmds – 1)
    {
    close(new_fds[0]);
    dup2(new_fds[1], 1);
    close(new_fds[1]);
    }

    //if this is first time in loop
    if(i == 0)
    {
    //if pipe-type is in_redirect, open file and write to new pipe
    if(strcmp(cmd_types[i], " 0)
    write(1, &buffer, 1);
    }

    //else if pipe-type was not in_redirect, then simply execute commands section
    else
    execvp(temp_cmds[0], temp_cmds);
    }

    //not first time in loop
    else
    {
    //if pipe-type was “|”, then execute commands section
    if(strcmp(cmd_types[i - 1], “|”) == 0 || strcmp(cmd_types[i - 1], “”) == 0)
    {
    file = open(temp_cmds[0], O_WRONLY | O_CREAT, 0666);

    //while reading from pipe and end of pipe not reached, write to file
    while((char_count = read(0, &buffer, 1)) > 0)
    write(file, &buffer, 1);
    }
    }

    exit(0);
    }

    //parent process
    else
    {
    //if there was a prevoius commands section, close old pipe
    if(i > 0)
    {
    close(old_fds[0]);
    close(old_fds[1]);
    }

    //if there is a next commands section, then set old pipe to new pipe
    if( i < num_cmds – 1)
    {
    old_fds[0] = new_fds[0];
    old_fds[1] = new_fds[1];
    }

    //if in last loop iteration, then wait for process id
    if(i == num_cmds – 1)
    waitpid(pid, NULL, 0);
    }
    }

    }

    bool check_quit(string str_cmd)
    {

    for(int i = 0; i < str_cmd.length(); i++)
    str_cmd[i] = tolower(str_cmd[i]);

    if(str_cmd == "quit" || str_cmd == "exit")
    return true;
    else
    return false;
    }

    bool checkfor_amp(string str_cmd)
    {

    for(int i = 0; i < str_cmd.length(); i++)
    if(str_cmd[i] == '&')
    {
    return true;
    }

    return false;
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>