Project 2a: The Unix Shell
There are three objectives to this assignment:
Read these updates to keep up with any small fixes in the specification.
When a command is not found in the path, do not print command not found, but rather the only error message.
By default, the path should be set to /bin so that the user can type a
few commands without first setting the path. Type
You do not have to support multiple commands being executed concurrently or being specified on the same command line; each command line will contain at most one command to run.
In this assignment, you will implement a command line interpreter (CLI) or, as it is more commonly known, a shell. The shell should operate in this basic way: when you type in a command (in response to its prompt), the shell creates a child process that executes the command you entered and then prompts for more user input when it has finished.
The shells you implement will be similar to, but simpler than, the one you
run every day in Unix. You can find out which shell you are running by typing
Basic Shell: WhooSH
Your basic shell, called
prompt> ./whoosh whoosh>
You should structure your shell such that it creates a new process for each new command (note that there are a few exceptions to this, which we discuss below). There are two advantages of creating a new process. First, it protects the main shell process from any errors that occur in the new command. Second, it allows for concurrency; that is, multiple commands can be started and allowed to execute simultaneously. However, in this project, you do not have to build any support for running multiple commands at once.
Your basic shell should be able to parse a command, and run the
program corresponding to the command. For example, if the user types
You might be wondering how the shell knows to run
Important: Note that the shell itself does not "implement"
The maximum length of a line of input to the shell is 128 bytes.
Whenever your shell accepts a command, it should check whether the command
is a built-in command or not. If it is, it should not be executed like
other programs. Instead, your shell will invoke your implementation of the
built-in command. For example, to implement the
So far, you have added your own
The formats for exit, cd, and pwd are:
[optionalSpace]exit[optionalSpace] [optionalSpace]pwd[optionalSpace] [optionalSpace]cd[optionalSpace] [optionalSpace]cd[oneOrMoreSpace]dir[optionalSpace]
When you run
You do not have to support tilde (~). Although in a typical Unix shell you
could go to a user's directory by typing
Basically, when a user types
% cd % pwd /afs/cs.wisc.edu/u/m/j/username % echo $PWD /u/m/j/username % ./whoosh whoosh> pwd /afs/cs.wisc.edu/u/m/j/username
The format of the path built-in command is:
[optionalSpace]path[oneOrMoreSpace]dir[optionalSpace] (and possibly more directories, space separated)
A typical usage would be like this:
By doing this, your shell will know to look inwhoosh> path /bin /usr/bin
Many times, a shell user prefers to send the output of his/her program to a
file rather than to the screen. Usually, a shell provides this nice feature
For example, if a user types
Here are some redirections that should not work:
ls > out1 out2 ls > out1 out2 out3 ls > out1 > out2
Note: don't worry about redirection for built-in commands (e.g., we will
not test what happens when you type
The one and only error message. You should print this one and only error message whenever you encounter an error of any type:
char error_message = "An error has occurred\n"; write(STDERR_FILENO, error_message, strlen(error_message));
The error message should be printed to stderr (standard error). Also, do not add whitespaces or tabs or extra error messages.
There is a difference between errors that your shell catches and those that
the program catches. Your shell should catch all the syntax errors specified
in this project page. If the syntax of the command looks perfect, you simply
run the specified program. If there is any program-related errors
(e.g. invalid arguments to
whoosh> ls whoosh> ls > a whoosh> ls > a
But not this (it is ok if this works, it just doesn't have to):
Defensive Programming and Error Messages
Defensive programming is required. Your program should check all parameters, error-codes, etc. before it trusts them. In general, there should be no circumstances in which your C program will core dump, hang indefinitely, or prematurely terminate. Therefore, your program must respond to all input in a reasonable manner; by "reasonable", we mean print the error message (as specified in the next paragraph) and either continue processing or exit, depending upon the situation.
Since your code will be graded with automated testing, you should print this one and only error message whenever you encounter an error of any type:
char error_message = "An error has occurred\n"; write(STDERR_FILENO, error_message, strlen(error_message));
For this project, the error message should be printed to stderr. Also, do not attempt to add whitespaces or tabs or extra error messages.
You should consider the following situations as errors; in each case, your shell should print the error message to stderr and exit gracefully:
For the following situation, you should print the error message to stderr and continue processing:
Your shell should also be able to handle the following scenarios below, which are not errors.
All of these requirements will be tested extensively.
Writing your shell in a simple manner is a matter of finding the relevant library routines and calling them properly. To simplify things for you in this assignment, we will suggest a few library routines you may want to use to make your coding easier. You are free to use these routines if you want or to disregard our suggestions. To find information on these library routines, look at the manual pages.
Parsing: For reading lines of input, once again check out fgets(). To open a file and get a handle with type FILE * , look into fopen(). Be sure to check the return code of these routines for errors! (If you see an error, the routine perror() is useful for displaying the problem. But do not print the error message from perror() to the screen. You should only print the one and only error message that we have specified above ). You may find the strtok() routine useful for parsing the command line (i.e., for extracting the arguments within a command separated by whitespaces).
Executing Commands: Look into fork, execv, and wait/waitpid. See the man pages for these functions, and also read book chapter here.
You will note that there are a variety of commands in the
int main(int argc, char *argv);
Note that this argument is an array of strings, or an array of pointers to characters. For example, if you invoke a program with:
foo 205 535
and assuming that you find
Important: the list of arguments must be terminated with a NULL pointer; in our example, this means argv = NULL. We strongly recommend that you carefully check that you are constructing this array correctly!
For managing the current working directory, you should use getenv,
chdir, and getcwd. The
Redirection is relatively easy to implement. For example, to redirect standard output to a file, just use close() on stdout, and then open() on a file. More on this below.
With a file descriptor, you can perform read and write to a file. Maybe in your life so far, you have only used fopen() , fread() , and fwrite() for reading and writing to a file. Unfortunately, these functions work on FILE* , which is more of a C library support; the file descriptors are hidden.
To work on a file descriptor, you should use open() , read() , and write() system calls. These functions perform their work by using file descriptors. To understand more about file I/O and file descriptors you should read the Advanced Unix Programming book Section 3 (specifically, 3.2 to 3.5, 3.7, 3.8, and 3.12), or just read the man pages. Before reading forward, at this point, you should become more familiar file descriptors.
The idea of redirection is to make the stdout descriptor point to
your output file descriptor. First of all, let's understand the
STDOUT_FILENO file descriptor. When a command
To check if a particular file exists in a directory, use the
Remember to get the basic functionality of your shell working before worrying about all of the error conditions and end cases. For example, first get a single command running (probably first a command with no arguments, such as "ls"). Then try adding more arguments.
We strongly recommend that you check the return codes of all system calls from the very beginning of your work. This will often catch errors in how you are invoking these new system calls. And, it's just good programming sense.
Beat up your own code! You are the best (and in this case, the only) tester of this code. Throw lots of junk at it and make sure the shell behaves well. Good code comes through testing -- you must run all sorts of different tests to make sure things work as desired. Don't be gentle -- other users certainly won't be. Break it now so we don't have to break it later.
Keep versions of your code. More advanced programmers will use a source control system such as git. Minimally, when you get a piece of functionality working, make a copy of your .c file (perhaps a subdirectory with a version number, such as v1, v2, etc.). By keeping older, working versions around, you can comfortably work on adding new functionality, safe in the knowledge you can always go back to an older, working version if need be.
To ensure that we compile your C correctly for the demo, you will
need to create a simple makefile; this way our scripts can just
The name of your final executable should be
Copy all of your .c source files into the appropriate subdirectory. Do not submit any .o files. Make sure that your code runs correctly on the linux machines in the galapagos (and similar) labs.
There is another contest for the shell. In your previous contest, you had to write the fastest sort. Now, you have to write the shortest, completely working, readable shell. What do we mean by readable? Well, the code should not be short because you have removed all white space and made it so we cannot understand what the code does. Rather, it should be short because you have removed all unnecessary redundancy. We will count the code by the number of (non-whitespace) lines; we will then examine the best few entrants to see whose code is actually readable but compact, and then choose a winner. Winner, as usual, gets a FAMOUS 537 T-SHIRT. Good luck!
We will run your program on a suite of test cases, some of which will exercise your programs ability to correctly execute commands and some of which will test your programs ability to catch error conditions.