The purpose of this assignment is to refresh your skills in Java programming and introduce you to threads and processes. You are to implement a simple shell (command interpreter) that behaves similarly (but not identically) to the UNIX shell. When you type in a command (in response to its prompt), it will create a thread that will execute the command you entered. Multiple commands can be entered on a single line, separated by '&' (ampersand) characters. Your shell will run them all concurrently and prompt for more user input when they have all finished.
You do not need to implement
pipes or re-direction of standard input and standard output, and & at
the end of a command has no special meaning (it does not mean to run the
command in the "background").
You must be able to handle an arbitrary number of commands per line --
each with an arbitrary number of arguments separated by arbitrary
amounts of white space (blanks or tabs).
Your program
should recover gracefully from such errors as unknown commands
by printing an error message and continuing.
Suggestions
Note: Addtional hints and explanations may be found on the FAQ (Frequently Asked Questions) page for this project. You should get in the habit of checking this page daily, as new hints and clarifications may be added at any time.
This project is not as hard as it may seem at first reading, because most of the hard parts have already been done for you by the standard Java library. It is simply a matter of finding the relevant library routines and calling them properly. Your finished program will probably be about 200 lines, including comments. Don't forget, this project is meant primarily to be a "get acquainted with Java and threads" exercise. It is worth only 2% of your grade for the course.
The public static void main() procedure in your primary class
will be quite simple. It will be an infinite loop1
that prints a prompt, reads a line,
parses it (breaks it up into its constituent commands),
starts a new thread to handle each of the different commands,
and then waits for all the threads to finish before printing the next prompt.
Parsing
For parsing, you may find it easier to read the entire line into a String
object. The stream
System.in,
which gets the keyboard input, is of type
InputStream,
so it can read either single bytes or arrays of bytes.
You could represent an input line as an array of bytes, but you will find
it much easier to use a
String
instead. You may want to look up the class
BufferedReader
to figure out how to read a line into a String.
Splitting the line into commands separated by & characters is easily
accomplished with the methods
indexOf and
substring in
class String, or with
StringTokenizer(str,delim).
Splitting the individual commands into words is almost trivial with
StringTokenizer.
Commands
Once you have split a command into words, it is easy to get the system to execute it with the aid of the classes Runtime and Process. Use r = Runtime.getRuntime() to get a reference to a Runtime object, and then call p = r.exec(argv) to run a command. Here argv is an array of Strings containing the words of the command (the command name itself is argv[0]) and the result p is a reference to a Process object.
There's one small catch. The output of the command will disappear down a black hole unless you go to the trouble of getting it and printing it to the screen. The way to get the standard output of the command is to call p.getInputStream(), where p is the Process reference returned by Runtime.exec(String[]). When you read from the resulting stream, you get the standard output from the child process. Some commands send some of their output to an alternative output stream called the "standard error stream". For example, the command cat foo sends the contents of file foo to standard output, but the file foo doesn't exist, cat prints an error message to its standard error stream. You can get the standard error output by calling getErrorStream().
You can also feed characters to the standard input of the process with getOutputStream(), but that is not required for this project.2 You do not need to be able to run commands that read from their standard input.
A process may produce both standard and error output and they may come in any order, even interleaved, so you will need to use two treads (per command) to print them both out.
The exit command should cause your program to terminate immediately. It will not work to use Runtime.exec(String[]) for exit (why not?), so you must look for it explicitly and use System.exit() to terminate the program. As with any other concurrent execution, if the exit command is run concurrently with other commands, the exact ordering of events is unpredictable. For example, in
cat foo & exityour shell may terminate before or after displaying the contents of file foo or even half-way through.
Your primary class will read a command line from a user and create threads to carry out the commands on the line. It will then wait until all the threads have finished before continuing its own execution. For this purpose, you will need a Runnable class Command so that you can create a Thread using code such as 3
Thread t = new Thread(new Command(/* whatever */)); t.start(); /* and later ... */ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(): }
To display both the standard and error output of the commands you will need
at least two threads per command. You may find it simpler to use three
threads: one thread for standard output, one for standard error, and a master
thread to create them and wait for them to finish.
If you find all of this very confusing, forget about standard
error, only dump standard output, and only create one thread per command.
Once you get this version debugged, you will probably not have much trouble
modifying your program to handle standard error properly.
Exceptions
Java requires you to place within a try block any methods that might
cause an exception. Following the try block is a catch clause
(or catch clauses) that will
be used to catch any exceptions that have been thrown
See Notes on Java
and Sections 1.13 and 10.12 of the Java book for more information about
exceptions.
Your code should deal with
exceptions in an appropriate manner. For example, exceptions such as
attempting to open a file that does not exist should
result in a message to the user and the continuation of the program.
More serious exceptions may require an error message followed by program
termination (using
System.exit()).
Grading
Note: For the remaining projects in this course, students will be working in two-person teams, but for this project, each person should work alone.
For this project, the grading will be 50% correctness (correctly implmenting the specifications), 40% style, and 10% testing. Later projects will have less emphasis on style, but we want to get you started off on the right foot and break any bad habits you may have. In particular,
Be sure that you use test data adequate to exercise your program's capabilities. Test both normal situtations and "boundary" cases: empty commands, very long commands, etc. In some cases, poor testing can cost you points not only for "testing", but also for "correctness"; if a feature is never tested, the grader may assume it doesn't work.
You may debug your program on any computer you have access to that supports Java, but you should test that it works correctly on the Centos Linux machines. Despite the Java hype about "Write once, run anywhereTM", Java implementations on these machines do behave somewhat differently.
If you are registered for this class, you will find that you have access to a directory ~cs537-1/handin/NAME, where NAME is your login name. For example, if your login is lab, your handin directory is ~cs537-1/handin/lab. Your handin directory has six subdirectories: P1, P2, P3, P4, P5, and late. For this assignment, use directory P1. Copy all your .java source files and any other required files into the handin directory. Do not submit any .class files. After the deadline for this project you will be prevented from making any changes in this directory. If your assignment is late, put it into the late subdirectory and send email to me for permission to have it accepted.
Hand in your source program and a transcript of a terminal session which demonstrates your shell's ability to perform as specified (use the Unix command script(1)): Simply type the command "script". You will see the message
Script started, file is typescriptAfter that, everything you type in and everything sent to the screen will be saved in the file "typescript". When you're done with your demo, type "exit" (to the Unix shell, not to your program!), and you should get the message
Script done, file is typescriptCopy the typescript file to the handin directory.
Since it is tedious to type the same sequence of commands to you shell over and over, you might want to create a file of commands, say testcommands, and use it to drive your program
java Project1 < testcommandsNote that if you do that, the commands themselves will not appear in the output, only prompts and the results of running the commands. That's ok, but don't forget to copy the file testcommands to the handin directory. Be careful that your program correctly handles end-of-file as explained in the FAQ.
When a command line has multiple commands, they must be run concurrently. If they produce output, the output may be interleaved in arbitrary ways. However, with commands like cat, you may not see any interleaving unless the files being printed are very long. To make the concurrency easier to see, we have provided a program GenOutput that produces output on both the standard output and standard error streams, randomly switching back and forth and introducing delays. To use it, visit the page GenOutput.java with your web browser and save the file as GenOutput.java with the "Save As..." command from the File menu, or copy the file /u/c/s/cs537-1/public/examples/GenOutput.java to your directory. Compile it with the command
javac GenOutput.javaThen type
java GenOutput junk 1000as an input line to your command interpreter. You should see stdout and stderr output randomly mixed. Similarly,
java GenOutput one 1000 & java GenOutput two 1000should mix the outputs of the two commands together.
1In other courses, a program with an infinite loop is considered a bad thing, but in Operating systems, it's the norm!
2Notice that the names are backwards: You use getOutputStream to connect to the standard input of the process and getInputStream to connect to the standard output. The names were apparently chosed because getInputStream returns an InputStream. Then again, so does getErrorStream. Go figure.
3It is also possible to declare that Command extends Thread and use Thread t = new Command(/* whatever */), but we do not recommend this way of creating threads for reasons too complicated to go into here.