CS170 Jshell Lab

This page last updated Thu Sep 27 14:10:56 PDT 2007

Lab Description

Your job is to write a very primitive shell named jshell (after the lab's progenitor Dr. James Plank). A shell is a command line interpreter and is an essential tool for an interactive OS. It reads sets of commands typed at a prompt (the command line) that requests one or more programs be started with input and output using a terminal, a file, or another program. It then issues a set of commands to the operating system to service the request or report any errors. You use a shell like this every time you log into a computer at CSIL and bring up a terminal.

The program you will write has these requirements. It will be started with the command jshell [ prompt ]. If the optional prompt is not specified, the prompt will be "jshell: ". For example, invoking jshell with no argument, produces the following prompt

./jshell
jshell:
but invoking it with the argument cs170: produces
./jshell cs170:
cs170:
You will be provided a parser (in a separate file) that will read in one line of commands that will consist of executables and arguments and the tokens <, >, >>, &, and |. The parser will interpret these using c shell semantics and produces a list of programs to be executed and files for input and output. You will then create all of these processes with the proper redirection of input and output. The shell should exit when the user types CTRL-D or exit.

When this program is finished, you should be able to run a session that looks like this:

calvin:labs/cs170/jshell> jshell
jshell: pwd
/cs/student/msa/labs/cs170/jshell/
jshell: cat > f1
this is a test
once I had a girl on rocky top
jshell: cat f1 | sort
once I had a girl on rocky top
this is a test
jshell: sort < f1 | head -1 | cat -n
     1  once I had a girl on rocky top
jshell: exit
calvin:labs/cs170/jshell>

A working for executable for Linux can be found at /cs/class/cs170/bin/jshell

Using the Parser

The parse function is declared as int parse(CMD **) in parse.h. The argument is a pointer to a pointer to a CMD struct that needs to have been declared already. When parse() returns, it will have stored the address of the first command in the command list in the argument. If there is an error in the command line, then parse will not create a command list and will instead return a negative integer. The error codes are defined in the header file. Here is an example of how to use this command:

CMD *commands;
int error_code;

error_code = parse(&commands);

The command struct is pretty straight forward. It looks like this:

typedef struct command_struct
{
    struct command_struct * prevCmd;
    struct command_struct * nextCmd;
    char * file;
    char ** argv;
    char * in_redir;
    char * out_redir;
    int appending;      /* flag, non-zero = append, zero = truncate */
    int background;     /* whether the line terminates with an & */
} CMD;

Each pipe-separated (|) executable command will have a structure like this that describes it. The file field is the executable to be run, and the argv field contains the NULL terminated argv that should be passed to the program. If there is an input or output redirect, the file name will contained in in_redir or out_redir respectively. The appending flag tells you if output should append to or truncate the output file, and the background flag tells whether the commands should be run in the background. The prevCmd and nextCmd fields point to the previous and next commands in the pipeline. Thus, the output of a command that is not the last command should go to nextCmd, and the input from any command but the first should come from prevCmd.

The other function in the file, int cmd_free(CMD *), is used to free the memory used by the command list. Use it to free every list generated by the parse() function. It would be wise to figure out the parser first before you work on the system calls. Any further question on the parser should be directed to the source code in parse.c

System Calls and Process Creation

The shell runs a program using two core system calls: fork() and execvp(). Read the man pages to see how you need to use the calls (use the command: man 2 fork). In short, fork() creates an exact copy of the currently running process, and is used by the shell to spawn a new process. The execvp() call is used to overload the currently running program with a new program, which is how the shell turns this new process into the program you want to run. Look at the files forkcat1.c and forkcat2.c to see how this is done.

Input and output redirection, either to files or pipes, is done using the dup2() command. This command allows you redirect a file descriptor to point to another file descriptor. For this lab you will create a file descriptor using the open() or pipe() system call and then set stdin (0) or stdout (1) to point to the new descriptors. Then, when you call execvp(), the new process will read or write from the file or pipe thinking it is the standard input or output. Look at the headsort.c program for an example.

The shell must wait until all of the previously started programs complete unless the user runs them as background processes (with &). This is done with the wait() system call. Also, the shell will need deal with processes that become zombies. Zombie processes are processes that have exited, but the OS keeps them around until the parent (the shell) checks their exit status. You should use wait4() or waitpid() with the non-blocking option set to clean up any zombie processes before you start new processes.

All of these functions are system calls, which means they need to be tested to see if they fail when you use them. The man pages will tell you what the call should return on success (man 2 function). If there is an error you need to report it with the perror() function.

Important Details

Here are a few important points to keep in mind as you work on a final submission for this lab: