CIT 5950 (Spring 2025) Home Schedule Assignments Tools & Refs HW 05: pipe shell

Building on top of retry shell to get a better understanding of processes, file descriptors and pipes.

Goals

Collaboration

For assignments in CIT 5950, you will complete each of them on your own or solo. However, you may discuss high-level ideas with other students, but any viewing, sharing, copying, or dictating of code is forbidden. If you are worried about whether something violates academic integrity, please post on Ed or contact the instructor.

Contents

Setup

You can downlowd the starter files into your docker container by running the following command:

curl -o pipe_shell.zip https://www.seas.upenn.edu/~cit5950/current/projects/code/pipe_shell.zip

You can also download the files manually here if you would like: pipe_shell.zip

From here, you need to extract the files by running

unzip pipe_shell.zip

Overview

In this assignment, you will be implementing a very simplified version of the UNIX shell (terminal) that you have been using to compile, run, and debug your code previously in the course.

This shell in some ways is similar to what you did in retry_shell. It will support the same basic functionality but instead of the “retry” functionality, we are instead supporting commands with pipes in them.

The shell you write will need to read commands from standard input, handle the execution of any programs in that input, and facilitate piping input from the stdout of one program to the stdin of another program. For example, if the user inputs the line ls | wc, your shell should fork off two programs (one for ls and one for wc) and establish a pipe from ls to wc. Similarly, if someone were to run ls | head | pipe, then you should fork three processes and setup two pipes. This shell that you write will not need to implement most of the complexities of a standard UNIX shell, things like environment variables, or shell features like &, >, >>, <, &&, ; and many other command line symbols.

Instructions

Among the starter files you will find:

For pipe_shell.cpp there are some specific requirements:

  1. Your program should read in commands from stdin (e.g. the cin stream) one line at a time. A command consists of a sequence of programs separated by the pipe character |.
  2. Continue reading and executing commands until you read EOF from stdin or exactly exit is input on one line.
  3. Wait for the current command (sequence of programs) to terminate before starting the next command.
  4. The child programs of a command must execute in parallel.
  5. If the command we try to run is invalid (e.g. we try to run a command that does not exist) then an error should be printed to stderr (cerr) and the shell should reprompt.
  6. Programs can be named by either an absolute path or just by the program name (execvp should handle this for you).

If you are not sure of whether you should do certain behaviour, you can probably figure out what is supposed to happen by running it in your terminal directly. E.g. running sleep 5 | sleep 10 in your terminal does the same thing that pipe_shell should do (the whole program should run for 10 seconds before reprompting). Please ask on ed if you have any questions about these or any other requirements/constraints for your program.

You can make any changes to pipe_shell.cpp to implement your shell.

Suggested Approach

Below we have provided a suggested approach to this homework. Note that you are not required to follow this ordering if you believe another approach would work better for you.

Also note that you can gradually check your progress by testing each part as you implement it. This isn’t always feasible, but we highly recommend doing so when possible.

  1. Start by READING THE ENTIRE SPECIFICATION. It shouldn’t be too long and will help with your understanding of the assignment
  2. Take a look at the lecture code two_pipes.cpp. Make sure you understand what is happening in this program and try running them yourself. You should also look at your retry_shell.cpp code from the previous assignemnt and think about how it should be modified for this assignment.
  3. Start implementing pipe_shell.cpp and start by prompting the user for input by printing out $ . Have your program continually loop reading in a line from the user, printing it back out to them and then re-prompting the user. stdin_echo may be useful to look at while doing this.
  4. Modify your program so that it supports basic shell functionality (no pipe yet). You should not have any rety functionality in your code.
  5. Modify your program to detect for and handle the case where a command has two programs as inputs that are separated by a | character (e.g. ls | wc). If the | is detected, then your code should fork both commands with a pipe running between the two.
  6. Modify your program to detect a command with three programs and two pipes (e.g. ls | cat | wc ).
  7. Generalize your code to handle any number of | and programs in a single command input. This step is the most complicated step.

Hints

Suggested Headers

Below is a list of suggested headers and functions or objects in those that you may find useful to use.

C++ Standard Library

C System Calls

Grading & Testing

Compilation

We have supplied you with a Makefile that can be used for compiling your code into an executable. To do this, open the terminal in codio (this can be done by selecting Tools -> Terminal) and then type in make.

You may need to resolve any compiler warnings and compiler errors that show up. Once all compiler errors have been resolved, if you ls in the terminal, you should be able to see an executable called pipe_shell. You can then run this by typing in ./pipe_shell and passing in various inputs to test your code your code.

Note that your submission will be partially evaluated on the number of compiler warnings. You should eliminate ALL compiler warnings in your code

Valgrind

We will also test your submission on whether there are any memory errors or memory leaks. We will be using valgrind to do this. To do this, you should try running: valgrind --leak-check=full ./pipe_shell

If everything is correct, you should see the following towards the bottom of the output:

 ==1620== All heap blocks were freed -- no leaks are possible
 ==1620==
 ==1620== For counts of detected and suppressed errors, rerun with: -v
 ==1620== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

If you do not see something similar to the above in your output, valgrind will have printed out details about where the errors and memory leaks occurred.

Testing

To test your implementation of pipe_shell you can compare the behaviour/output of it to running the same commands in your terminnal.

Additionally, we have provided a few sample inputs and outputs in tests directory that you can use for testing purposes. You can use the provided test files to automate the comparison of results.

For instance, if you wanted to test your code on the simple test case, you can run

cat ./tests/simple_input.txt | ./pipe_shell &> my_output.txt

and then compare the file my_output.txt to ./tests/simple_output.txt.

Reading the expected output of these can be a bit difficult though since the expected output files don’t contain the user input. To avoid this, you can use the diff program which comparse two files and prints any difference between them (or nothing if they are the same). So one could do

diff my_output.txt ./tests/simple_output.txt

You can combine this with the previous command to do it all on one line with:

cat ./tests/simple_input.txt | ./pipe_shell &> my_output.txt && diff my_output.txt ./tests/simple_output.txt

Please don’t hesitate to post on Ed if you are having troubles with testing your code!

Submission:

Please submit your completed pipe_shell.cpp to Gradescope