Lab: Shell

Assigned
  • February 1, 2019
Due
  • February 8, 2019 by 10:30pm
Submitting
Submit your completed shell on Gradescope. You only need to upload your mysh.c file. Please don’t forget to select your lab group members when you submit your lab.

Overview

In this lab, you will implement a simple “shell” program. A shell is an interactive program that allows you to start and manage other programs, serving as a sort of intermediary between you, the OS, and the running processes. When you run a program in the terminal, you are actually interacting with the shell.

The shell you write for this program (called mysh) will support basic functionality that allows you to run programs in the foreground and background, and to move around the filesystem.

The starter code for this lab is available in the archive shell.tar.gz. Download and extract this archive as you did with the previous lab.

As you work through this lab, make sure you understand and handle all of the error conditions for the POSIX functions you call. A component of your grade on this lab will be determined by your use of good coding style, including checking for and handling error conditions in a reasonable way.

Part A: Reading in Commands

The starter code includes a simple command prompt loop that reads in a line of text and prints it back out. The first step in actually executing these commands is to break them into pieces. Take a look at the manpage for execvp, which is the variant of exec we will use for this lab. This function takes in a command name or path and an array of arguments than ends with a NULL argument. Write code to break the command line string into an array of char*s ending with a NULL entry. You can do this with strsep or strtok_r. You may assume there will be no more than MAX_ARGS parts to any one command. Please use the MAX_ARGS constant defined in mysh.c so this number can be changed without replacing constants sprinkled throughout your code.

You do not need to support quoted arguments in your shell.

Part B: Launch a Process

Now that you can read in and break apart commands, use fork to create a child process, execvp to launch the command in the child process, and wait to have the shell wait for the child process to complete. The reason we are using the execvp function for this lab is so we do not need to implement path resolution, the process of searching through directories in the PATH environment variable to find an executable that matches the given command name.

The convention for exec is that the first argument passed to the program is the name of the program. For example, the command ls /home/curtsinger runs the program /bin/ls with the arguments ls and /home/curtsinger.

When the child process completes, print the exit code for the child process before showing the command prompt again. For example:

$ ls
Makefile README.md common.mk mysh mysh.c obj
Child process 12802 exited with status 0
$ grep
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
        [-e pattern] [-f file] [--binary-files=value] [--color=when]
        [--context[=num]] [--directories=action] [--label] [--line-buffered]
        [--null] [pattern] [file ...]
Child process 12804 exited with status 2

Hint: Pay close attention to the meaning of the parameter you pass to wait. This value is not the same as the child process’ exit status!

Part C: Built-in Commands

While the command cd is actually an executable in /bin, this won’t work for our shell; when you run the command you do change directories, but this doesn’t change the working directory of the parent process (your shell). Instead, your shell will need to check if the current command is cd. If it is, your shell should call the chdir function to change directories instead of calling fork and exec to run /bin/cd in a child process.

You should also add special handling for blank lines (which should do nothing) and the exit command.

Part D: Multiple Commands

Most shells also allow you to invoke multiple commands in sequence using a semicolon. For example, cd ..; pwd; ls will move up a directory, print the full path of that directory, then print the files in that directory. Modify your shell to support multiple commands chained together with a semicolon.

I recommend using the strpbrk function to split commands at semicolons. The strpbrk function works like strchr, except it searches for one of a collection of delimeters. Conversely, you can think of it as similar to strsep, except it does not modify the input string by overwriting the delimeter. This is particularly useful when you go on to the next part of the lab, which requires that you treat commands differently depending on what delimeter separates them from the next command.

The following code shows how you can use strpbrk to split a string at both periods and commas.

#include <stdbool.h>
#include <string.h>

void split_string(char* str) {
  char* current_position = str;
  while(true) {
    // Call strpbrk to find the next occurrence of a delimeter
    char* delim_position = strpbrk(current_position, ".,");
    
    if(delim_position == NULL) {
      // There were no more delimeters.
      printf("The last part is %s.\n", current_position);
      return;
      
    } else {
      // There was a delimeter. First, save it.
      char delim = *delim_position;
      
      // Overwrite the delimeter with a null terminator so we can print just this fragment
      *delim_position = '\0';
      
      printf("The fragment %s was found, followed by '%c'.\n", current_position, delim);
    }
    
    // Move our current position in the string to one character past the delimeter
    current_position = delim_position + 1;
  }
}

Part E: Background Commands

Add support for background commands, which are launched with the & symbol. The most common use case for background commands is to launch a single executable without blocking the current shell. For example, the command sleep 5 will block the shell for 5 seconds before a prompt is printed again, while sleep 5 & will run the sleep command in the background and allow you to run additional commands immediately.

It is also possible to run multiple commands with a single invocation separated by an ampersand, much like a semicolon. Unlike the semicolon separator, the ampersand will run the joined commands simultaneously instead of in sequence. You can also combine semicolon and ampersand delimited commands, with the following rules.

  1. If the command ends with an ampersand, it runs in the background. The shell immediately runs the next command or prompts for the next command if there isn’t one.
  2. If the command ends with a semicolon, it blocks the shell until the command finishes.
  3. If the final command in the command line input does not have a semicolon or ampersand at the end, the behavior is the same as a command with a semicolon after it.

Hint: Don’t pass the ampersand or semicolon separator as an argument to the child process or built-in command.

Hint: These rules imply the semicolons and ampersands have the same precedence. Simply associate them with the command that precedes them.

When you have one or more background commands running, the shell should print the exit status of any command that quits between command invocations. For example:

$ sleep 1 & sleep 1; sleep 1
(shell pauses one second here)
Child process 12805 exited with status 0
Child process 12806 exited with status 0
(shell pauses one second here)
Child process 12807 exited with status 0
$ 

You can check for this behavior with a simple test in a real shell, as well as your mysh implementation. Type the command sleep 1 & and hit enter. Then, sit on the blank command prompt for well over one second. You should not see any indication that the one second sleep has finished. Then, hit enter to run a blank command. Before the next prompt appears, you should see the exit status of the sleep command.

You can use the wait function to block until a child process quits (a semicolon command), but you will need to use waitpid with the WNOHANG flag to check for any completed background processes. With this flag, the function will return immediately and indicate whether there are any additional zombie processes to collect. Do this repeatedly until there are no more zombies, then move on to the next command or prompt for a new one.

Extra Challenges: Subcommands, Pipes and Redirection

Another important feature of a shell is the ability to compose commands to perform more complex operations. These aren’t required for your implementation, but if you’d like an extra challenge you could add one or more of these features:

  1. Subcommands, which allow you to wrap a command in backticks (`) to use the output of that command as part of another command line.
  2. Redirection, which allows you to send the contents of a file as input to a command with <, or the output of a command to a file with >.
  3. Pipes, which allow you to use the output of one command as the input to another command with |.

To implement these features, you may need the functions dup2, open, close, and pipe. These use some file-related functionality that we haven’t talked about in class. I am happy to discuss these with you during office hours.

Questions & Answers

When I run ls in my shell it thinks I am passing the parameter "". What do I do?
Make sure you put NULL at the end of your args array, not "\0".
How should the shell run with ampersands between commands?
You don’t have control over which background jobs run in which order, so the behavior may be a bit unpredictable. Your shell just has to worry about starting commands in the appropriate mode (wait or don’t wait). That behavior is based on the trailing character. If the subcommand ends in ;, wait for it to finish before moving on. If it ends in &, move on right away. The examples in the lab using the sleep command are the easiest way to test your implementation that I could come up with, but if you have alterantives I would be happy to see them.
When should the shell print the exit status for child processes?
I’ll be flexible in how I grade this, so don’t worry too much if your shell is printing the status for all of the child processes. To match the examples, you should check for child processes any time you (a) move on to the next command in a sequence of ampersand- or semicolon-separated commands, or (b) you are about to ask the user for the next command. Make sure to collect all of the zombie child processes at this point, not just one.