COM3336
Programming Assignment I

Due:

January 19 at the start of class time: 6 p.m.

Contents


Introduction

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 & exit
your shell may terminate before or after displaying the contents of file foo or even half-way through.

Using Threads

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 typescript
After 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 typescript
Copy 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 < testcommands
Note 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.java
Then type

    java Dribble SOME STUFF & java Dribble some other stuff
as an input line to your command interpreter. You should see the outputs of the two Dribble commands randomly intermixed.
solomon@cs.wisc.edu
Thu Sep 24 13:50:47 CDT 1998