Lab: Shell

Assigned:
Tuesday, Jan 30, 2018
Due:
Tuesday, Feb 6, 2018 by 10:30pm
Collaboration:
Work with your assigned partner for this lab. You can use online resources for your lab, provided they do not provide complete answers and you cite them in your code. If you do not know whether it is acceptable use a specific resource you can always ask me.

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 on GitHub at https://github.com/csc213/shell. To start out, fork this repository and add your lab partner as a collaborator.

As you work through this lab, make sure you understand and handle all of the error conditions for the POSIX functions you call. Testing some error conditions is very difficult, so this will mostly depend on a careful reading of the relevant manpages.

Groups

Group information is no longer available for this course.

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 aobut 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.
Should we import the parser code?
No, you can just copy paste. There’s no need to cite this, since it’s in the lab.
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.

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 repeated calls to strtok, strtok_r, or strsep. You may assume there will be no more than 128 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.

Hint: You will need to revisit your command parsing code from part A to break a command line string into commands separated by semicolons. The code in the parser directory may be useful for this step, and the next step as well.

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.

Hint: There is code in the parser directory of the starter code that may be helpful for breaking commands containing semicolons and ampersands into their subcommands.

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 for 1 second here)
Child process 12805 exited with status 0
Child process 12806 exited with status 0
(shell pauses for 1 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 Credit: Subcommands, Pipes and Redirection

Another important feature of a shell is the ability to compose commands to perform more complex operations. You will receive 2% extra credit for each the following 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 |.

If you implement any of these features, include a note in your pull request to submit the lab saying which features you have implemented.

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.