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 >> file2ps ax | grep fooless 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
7 Comments
I have a C++ book on my bookshelf. One day I shall read it, and know what you’re on about. For now, I shall nod and agree. ;)
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. :)
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 :)
In your code you are saving the file to you local directory. Is there anyway to save it on host computer?
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.
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); } }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.