CS 31 Lab 7: Shell Program

Due 11:59pm, Tuesday, November 15


RTFM: often, but not always good advice.
Source: xkcd.com/293


Lab 7 Goals:


Lab Description

You will implement a shell, which is the program you interact with on the command line of a terminal window. A shell operates by doing the following things:

  1. Print a prompt and wait for the user to type in a command.
  2. Read in the command string entered by the user.
  3. Parse the command line string into an argv list.
  4. If the command (first item in the parsed argv list) is a built-in shell command, the shell will handle it on its own (without forking a child process).
  5. Otherwise, if it's not a built-in command, fork a child process to execute the command and wait for it to finish (unless the command is to be run in the background, then the shell doesn't wait for the child to exit).
  6. Repeat until the user enters the built-in command exit to exit the shell program.

You will be implementing steps 1, 2, 4, 5, and 6 above. Step 3 will be accomplished by a call to the parse_cmd library.

Using the parse_cmd library.

In parsecmd.h is the function prototype for the parse_cmd function that you can use to construct the argv array of strings from the command line string. The argv array is passed into execvp. The function comment describes how to call the function. There are also constant definitions that you can use in your shell. (Note: parsecmd.h is already #included at the top of cs31shell.c, so your code can use anything it defines.)

Note: The parse_cmd function is already implemented for you. It is compiled into the parsecmd.o binary that is linked into your cs31shell executable when you type make: you do not need to implement the parse_cmd function, but just call it in your code when you want to use it (much like you can call printf in your code without implementing the printf function).


Building a shell.

Your shell should support the following features:

  1. Running commands in the foreground.

    When a command is run in the foreground, for example:

    cs31shell> ./sleeper 2  
    

    Your shell program should fork() a child process to execute sleeper and then wait until the child process exits before proceeding. You can accomplish this by calling waitpid in the parent (your shell) by passing in the pid of the child process (the return value of fork()).

  2. Running commands in the background.

    When a command is run in the background, for example:

    cs31shell> ./sleeper 3 & 
    

    Your shell program should fork() a child process to execute sleeper, but it should NOT wait for the child to exit. Instead, after forking the child process, it should immediately return to step 1 (print out the prompt and read in the next command line). The child process will execute the command concurrently while the parent shell handles other command(s).

    Your shell must still reap background processes after they exit, so you can't just forget about them! When a child that was run in the background exits, your shell program will receive a SIGCHLD signal. You should install a SIGCHLD handler that will call waitpid() to reap the exited child process(es).

    Your shell should be able to run any number of processes in the background, so if you type in quick succession:

    cs31shell> ./sleeper & 
    cs31shell> ./sleeper & 
    cs31shell> ./sleeper & 
    cs31shell> ps 
    

    The ps program output should list all three sleeper child processes.

  3. Built-in commands.

    Your shell should respond to the following three built-in commands on its own. It should not fork off a child to handle these!

    1. exit: Terminate the shell program. You can print out a goodbye message, if you'd like.
    2. history: Print a list of the user's 10 more recently entered command lines. (Note: blank lines should not be added to the history.)
    3. !num (where num is an actual number, e.g., !5): Re-execute a previous command from the history. The previous command could be a run-in-the-foreground, run-in-the-background, or a built-in command that your shell should execute appropriately. The command line should be added into the history list (i.e. executing !5 should not put !5 in the history list, instead a copy of the command line associated with command ID 5 from the history list should be added to the history list). See the sample output below for some examples of history and !num commands.
  4. History.

    Your shell program should keep a list of the 10 most recently entered command lines by the user (use a #define constant named MAXHIST whose value is 10). The built-in history command should print out the history list in order from first (oldest) to last (most recently entered) command. For each element in the list, it should print the command ID and its corresponding command line string. The command ID is an ever-increasing number, and each command should increment the command ID by one. I suggest using an unsigned int to store this value (don't worry, I'm not going to try executing 4 billion commands in an attempt to overflow this value).

    Your history list should be implemented as circular queue of strings: a circular array of MAXHIST buckets where new commands are added to one end and old ones removed from the other end.

    Users can request the execution of a previous command, as stored in the history list, by executing the built-in !num, where "num" is a number corresponding to a history command ID. Upon receiving such a command:

    1. Check whether your history list contains a command with a matching command ID. Remember that the command ID is not the position in the history list, it is the unique number of the command in your shell's execution history (i.e. !5 is the 5th command run by your shell, !34 is the 34th command run by your shell). You will probably need an additional global variable to track this.
    2. If a matching command ID is not in the history queue, print out an error message.
    3. Otherwise, use the command line from the matching history command ID, and re-execute it (this command now also becomes the most recent command to add to your history list).
    4. If the command from the history list is not a built-in command, then your shell should run it just like it does any foreground or background program (parse its command line into argv list, fork-execvp and waitpid or not).

      If the command from the history list is a built-in command (which could only be the history command), then it should execute it directly just like any built-in command.


Requirements

Example Output

It may be helpful for you to take a look at Tia's sample output, particularly to see how the built-in history features work.


Tips


Submitting

Please remove any debugging output prior to submitting.

To submit your code, simply commit your changes locally using git add and git commit. Then run git push while in your lab directory. Only one partner needs to run the final push, but make sure both partners have pulled and merged each others changes.