Exercise 1: A Simple Shell

Assignment

Design and implement a simple, interactive shell program that prompts the user for a command, parses the command, and then executes it with a child process. In your solution you are required to use execv(), which means that you will have to read the PATH environment, then search each directory in the PATH for the command file name that appears on the command line.

The code (i.e. source files, header files, Makefile) for this asignment should be sent as a zipped tar file to Engelbert Hubbers not later than Thursday March 4th. On Tuesday March 9th, the shells need to be demonstrated during the exercise hours. Because of the coming holiday, these dates may seem far away. Do not make the mistake to start too late.

This exercise is based upon Lab Exercise 2.1 from Gary Nutt's Operating Systems.

Goals

After this exercise you should:

Background

The shell command line interpreter is an application program that uses the system call interface to implement an operator's console. It exports a character-oriented human-computer interface, and uses the system call interface to invoke OS functions.

Every shell has its own language syntax and semantics. In conventional UNIX shells a command line has the form

command argument_1 argument_2 ...
where the command to be executed is the first word in the command line and the remaining words are arguments expected by that command. The number of arguments depends on the command which is being executed.

The shell relies on an important convention to accomplish its task: the command is usually the name of a file that contains an executable program. For example ls and cc are the names of files (stored in /bin on most UNIX-style machines). In a few cases, the command is not a file name, but is actually a command that is implemented within the shell; for example cd is usually implemeted within the shell rather than in a file. Since the vast majority of the commands are implemented in files, think of the command as actually being a file name in some directory on the machine. This means that the shell's job is to find the file, prepare the list of parameters for the command, and the cause the command to be executed using the parameters.

A shell could use many different strategies to execute the user's computation. However the basic approach used in modern shells is to create a new process to execute any new computation.
This idea of creating a new process to execute a computation may seem like overkill, but it has a very important characteristic. When the original process decides to execute a new computation, it protect itself from fatal errors that might arise during that execution. If it did not use a child process to execute the command, a chain of fatal errors could cause the initial process to fail, thus crashing the entire machine.

Steps

These are the detailed steps any shell should take to accomplish its job (note that the code snippets are just examples; they might not suffice for this assignment, but they should give enough clues):

Code skeleton

It is good practice to use header files to define constants and types. In particular this means that you have to make decisions on the values of these constants. Here is an example minishell.h:

...
#define LINE_LEN     80
#define MAX_ARGS     64
#define MAX_ARG_LEN  16
#define MAX_PATHS    64
#define MAX_PATH_LEN 96
#define WHITESPACE   " .,\t\n"

#ifndef NULL
#define NULL ...
#endif

struct command_t {
  char *name;
  int argc;
  char *argv[MAX_ARGS];
}
And here is a skeleton of a very simple shell. For instance, it doesn't distinguish between internal commands and file commands. Your solution needs to be more sophisticated!
/* This is a very minimal shell. It finds an executable in the
 * PATH, then loads it and executes it (using execv). Since
 * it uses "." (dot) as a separator, it cannot handle file names
 * like "minishell.h".
 */
#include
#include "minishell.h"

char *lookupPath(char **, char **);
int parseCommand(char *, struct command_t *);
int parsePath(char **);
void printPrompt();
void readCommand(char *);
...
int main() {
  ...
/* Shell initialization */
  ...
  parsePath(pathv); /* Get directory paths from PATH */

  while(TRUE) {
    printPrompt();
    
  /* Read the command line and parse it */
    readCommand(commandLine);
    ...
    parseCommand(commandLine, &command);
    ...

  /* Get the full pathname for the file */
    command.name = lookupPath(command.argv, pathv);
    if (command.name == NULL) {
      /* Report error */
      continue;
    }

  /* Create child and execute the command */
    ...

  /* Wait for the child to terminate */
    ...

  }

/* Shell termination */
  ...
}