Note: Some links below to Java classes refer you to the Java 1.1 documentation. However, in this course we use Java 1.2. I left the links in because the part of Java that we use in this exercise has probably not changed much.
The purpose of this assignment is to introduce you to Java programming. 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, but 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
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 under 200
lines, including comments. Don't forget, this project is meant primarily
to be a "get acquainted with Java" exercise.
The public static void main() procedure in your primary class
will be quite simple. It will be an infinite loop that prints a prompt, reads
a line (in other courses, a program with an infinite loop is considered a bad
thing, but in Operating systems, it's the norm!),
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.
Scanning
For scanning, 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 object
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.
Splitting the individual commands into words is almost trivial with the
StringTokenizer
class found in
java.util.
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(). 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. You do not need to be able to run commands that read from their standard input. By the way, notice 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. Don't blame me; it's not my fault!
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 for exit (why not?), so you must look for it explicitly and use System.exit() and 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 carry out the commands on the line. It will then wait until the threads have finished before continuing its own execution. There are two ways to start threads in Java. The first is to derive your class from the Thread class and then override its run() function. The second is to use the Runnable interface. With this approach, you create a class that implements Runnable and pass an instance of this class into the constructor of a new thread object. Although the first approach seems simpler at first, it is has some pitfalls, so we recommend using the second approach.
To handle 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, we recommend you 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 Java for C++ Programmers
and Chapter 7 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
We will create a directory /course/com3336/handin/NAME, where NAME is your login
name. It has subdirectories P1, P2, P3, P4,
P5, and late.
For this assignment, your handing directory is /course/com3336/handin/NAME/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 (9:30am, September 17),
you will be prevented from making any changes in this directory.
If your assignment is late, please use the late subdirectory.
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 testcommands to the handing directory. Be careful that your program correctly handles end-of-file as explained in the FAQ.
Be sure that you use test data adequate to exercise your program's capabilities. You should follow all the principles of software engineering you learned in CS 302 and CS 367, including top-down design, good indentation, meaningful variable names, modularity, and helpful comments. You will be graded not only on the basis of correctness, but also programming style and completeness of test data.
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 Dribble that produces output a little at a time, with random pauses between chunks of output. To use it, visit the page Dribble.java with your web browser and save the file as Dribble.java with the "Save As..." command from the File menu, or copy the file /course/com3336/examples/Dribble.java to your directory. Compile it with the command
javac Dribble.javaThen type
java Dribble SOME STUFF & java Dribble some other stuffas an input line to your command interpreter. You should see the outputs of the two Dribble commands randomly intermixed.