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 that can handle these types of commands:
cat file1 >> file2
ps ax | grep foo
less file1
That is, you can do file redirects, pipes, and regular commands. We were not allowed to use the system()
function anywhere in our code.
general.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
/* 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
/* 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