CSCI 2330
Introduction to Systems

Bowdoin College
Spring 2017
Instructor: Sean Barker

Project 6 - The Bowdoin Shell

This project should be completed individually or in groups of up to 3. Groups should only complete and turn in a single program.

This project will help you understand the inner workings of the shell, a program that you have been using all semester. You'll do this by writing your own simple shell program that supports Unix-style job control. Implementing your shell will teach you the core concepts of process control and signaling, as well as give you experience with low-level system programming in C.

Start by reading through the entire project description!

Project Dates

AssignedSaturday, April 30.
DueFriday, May 12 (firm, no flex days).

Project Overview

A shell is an interactive command-line interpreter that runs programs on behalf of the user. At a high level, a shell repeatedly prints a prompt, waits for a program name and command-line arguments on stdin, then carries out some action as directed by the input.

The shell program that you have been using all semester is bash (the Bourne Again Shell). Bash is only one of many shell programs, however - others include sh, tcsh, and csh. In this project, you will implement your own shell, bsh (the Bowdoin Shell).

Unix Shell Basics

As we saw in project 2, a command-line string is a sequence of text words delimited by whitespace. The first word of the string is either the pathname of an executable file (i.e., a program) or a built-in command. The remaining words are command-line arguments. If the first word is a built-in command, the shell immediately executes the command within the current shell process. Otherwise, the shell forks a child process, then executes the specified program in the context of the child. The set of all child processes created as a result of interpreting a single command (there may be mulitple, if the program itself forks) are known as a job. A job can also contain multiple child processes connected by Unix pipes (denoted in a command by vertical bars, |), which allow for passing output from one program as input into another program.

By default, a job runs in the foreground, which means that the shell waits for the job to terminate before prompting for the next command string. Thus, at any point in time, at most one job can be running in the foreground. However, if the command string ends with an ampersanad (&), then the job runs in the background, which means that the shell does not wait for the job to terminate before printing another prompt and waiting for another command string. Thus, an arbitrary number of jobs can be running in the background at a given time.

Typing the following command runs the program ls (located in the directory /bin) in the foreground with command line arguments -l -d:

bsh> /bin/ls -l -d

Note that more specifically, calling the above will execute the main function of /bin/ls with the following values of argc and argv:

Alternately, typing the same command with an ampersand will run ls in the background:

bsh> /bin/ls -l -d &

Job Control

Unix shells support the notion of job control, which allows users to move jobs back and forth between background and foreground, and to change the process state (running, stopped, or terminated) of all the processes in a job. Job states can be changed via signals: typing Ctrl-Z causes a SIGTSTP signal to be delivered to every process in the foreground job. The default action for SIGTSTP is to place the process in the stopped state, where it remains until it is awakened by the receipt of a SIGCONT signal. Typing Ctrl-C causes a SIGINT signal to be delivered to each process in the foreground job. The default action for SIGINT is to terminate the process.

Unix shells also provide various built-in commands that support job control. Key commands are listed below:

The bsh Specification

The bsh shell should have the following features:

Code Structure

To start, you have been provided with a functional skeleton of the shell. The starting code implements a number of less interesting functions (such as command line parsing and error reporting) that you should use while implementing the complete shell, allowing you to focus on the more interesting components. In particular, you are responsible for implementing each of the empty functions listed below. To give you an idea of the complexity of each function, also listed below is the number of code lines implementing each function in my reference shell (including comments):

Note: While the function lengths given above are fairly modest, don't be lulled into a false sense of security! System programming involves writing dense, precise, and often error-prone code, and is likely to require significant debugging time.

The single file you should modify that contains the code of your shell is bsh.c. The included Makefile will compile the shell for you. To run your shell, simply execute it:

unix> ./bsh
bsh> [type commands to your shell here]

Included Files

You have also been provided with a number of tools to help you check your work. All included files are described below:

Use the -h flag to see the usage string for

unix> ./ -h
Usage: ./ [-hv] -t <trace> -s <shellprog> -a <args>
  -h            Print this message
  -v            Be more verbose
  -t <trace>    Trace file
  -s <shell>    Shell program to test
  -a <args>     Shell arguments

For example, you could run the shell driver on trace01.txt by typing the following:

unix> ./ -t trace01.txt -s ./bsh

Similarly, you could run the trace driver on the reference shell by simply substituting bsh with bshref in the command above.

More simply, you can use the included Makefile to run the driver on the trace files. To pass trace01.txt through your shell, you can just run:

unix> make test01

Similarly, to pass trace01.txt through the reference shell, you can run:

unix> make rtest01

The output of your shell from the trace files is exactly the same as the output you would have gotten from running your shell interactively, except for an initial comment that identifies the trace.

Output Formatting and Logging

As stated above, the output of your shell should exactly match that of the reference shell (except for PIDs, which will be different). This means that your shell's own messages should contain the same information in the same format as the reference shell.

Particular messages that you should look out for include the following (refer to the reference shell if you are unsure about any of the exact formatting -- trace 14 in particular exercises many of the error messages):

Note that the above messages should always be printed -- you are welcome to add additional logging in verbose mode, but this output does not need to match that of the reference shell.

General and Function-Specific Advice

Here are some useful tips for working on your shell:

Tips for specific parts of the shell are given below.


Signal Handlers

Using GDB in a Multi-Process Program

To debug a multi-process program such as your shell using gdb, you will need a few extra commands:

(gdb) set detach-on-fork on/off

The above command sets whether child processes will be detached when fork is called. The default is on (i.e., the child runs without any interruption). If you turn this option off, the child is suspended as soon as it is forked. Then, you can use the inferior command to switch between the various processes started by the shell:

(gdb) info inferiors
... listing of processes ...
(gdb) inferior 1
[Switching to inferior 1 [process 0] (<noexec>)]

Another useful option is the following:

(gdb) set follow-fork-mode parent/child

The above sets which process gdb will automatically follow (either the parent or the child -- parent is the default) after fork is called.


You are responsible for completing the contents of the bsh.c file. You should not modify any other file. You are responsible for ensuring that your program runs on the class server, regardless of where else you may be writing code. Since other systems may have the same system calls but with slightly different behavior, you are strongly urged to develop your code entirely on the class server.

As usual, your final submission will consist of your committed bsh.c file at the time of the due date. Each group need only make one submission.

warning Note for groups: If you are working in a group, you must individually send me an email giving me a brief description of your and your group member's contribution to the project after your submission. The purpose of this requirement is to encourage full group participation on the project. Your email does not need to be detailed, but your project is not considered complete until your summary is received. Your email will be kept confidential by me and not shared. However, in the case of a clearly inequitable distribution of work, I may adjust individual grades up or down from the group score.


You will be evaluated both on the correctness of your shell implementation (as determined by the 16 trace files) as well as the style and overall quality of your program (as determined by me).

Particular things to watch out for:

In addition, you should follow all standard and sensible style guidelines, such as using good variable names, commenting, using consistent indentation, etc. While you do not have to adhere to any particular set of conventions (e.g., where to put parentheses), you SHOULD be consistent with whatever conventions you choose. If you are working in a group, agree on a single set of conventions and stick to them!

You do not need to define any functions beyond those already specified in bsh.c, but you are welcome to do so if you wish.

Please ask if you have any questions about what contitutes good style or what is expected!