CIT 5950 (Spring 2025) Home Schedule Assignments Tools & Refs HW 04: retry shell

Getting a better understanding of processes and the terminal by implementing a relatively simple shell

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 retry_shell.zip https://www.seas.upenn.edu/~cit5950/current/projects/code/retry_shell.zip

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

From here, you need to extract the files by running

unzip retry_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.

The shell you write will need to read commands from standard input, handle the execution of the program in that input, and implements a “retrying” feature.

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 piping with |, &, >, >>, <, &&, ; and many other command line symbols.

retry_shell will be demonstrated in lecture if it hasn’t been demonstrated already. We refer you to that to see exactly how things should be handled.

Basic Features

Your shell should support basic functionality to run any commands that we pass in once we start retry_shell. This is similar to how you can type commands into the terminal you are developing on and those commands will run. Below is an example execution of our retry_shell.

root/workspace/retry_shell$ ./retry_shell
$ echo hello
hello
$
$ sleep 3
$ echo I am doing well!
I am doing well!
$ exit
root/workspace/retry_shell$ 

Lets walk through what is going on in this example:

  1. First line root/workspace/retry_shell$ ./retry_shell is us starting our retry_shell. The following lines are what happens when we run retry shell.
  2. Second line $ echo hello. First the program is prompting us with $ and then the user types in echo hello and hits enter.
  3. The third line is the output of running the program echo hello
  4. the fourth line starts with the retry_shell reprompting us again, but this time the user just hits the enter key. When there is no non-white-space characters, then the shell reprompts (which we see on the next line)
  5. The fifth line starts with a reprompt $ and then this time the user types in sleep 3 and hits enter. At this point the program sleep 3 is run in the terminal. This program doesn’t print anything, just waits for 3 seconds.
  6. The sixth line starts with a prompt $ and the user typing in echo I am doing well! before hitting enter
  7. The seventh line is the output of running the command echo I am doing well! from the previous line.
  8. The eighth line again prompts the user and the user types in exit and hits enter. This exits the shell and on the next line it stops running retry_shell and goes back to the normal terminal.

Note that instead of typing in exit a user could hit CTRL + D (or Command + D on Mac) and that should end the shell.

You should implement your retry_shell to support this set of basic functionality.

It may be useful to look at how lecture and recitation code handled fork()-ing and execvp()-ing processes.

We encourage you to look at the section below on Suggested Approach for hints as to how to handle this.

Retry Feature

One feature that is unique to this shell is that you can pass in an command line argument to this program that enables a retry feature. So instead of running ./retry_shell you run ./retry_shell <some positive integer> for example ./retry_shell 3. When a positive number is passed in, your shell should then use that number as a “retry count”. What this means is that your shell should run normally, but it checks every command run to see if it exited normally or not (It does not check what exit status it had, just that it exited normally). If it did not exit normally then it will re-run the command N (the passed in command line arg) times or until it properly exits normally.

If no command line argument is provided (e.g. it is just run as ./retry_shell) then no retrying is done and it runs with just basic functionality.

We can demonstrate this functionality below. Note that from the shell we run counter which is a program we provide you that is killed the first four times it runs and then exits normally on the fifth run.

root/workspace/retry_shell$ ./retry_shell 2
$ ./counter
Not done yet...
retrying...
Not done yet...
retrying...
Not done yet...
Failed to run program after retrying
$ ./counter
Not done yet...
retrying...
$ exit
root/workspace/retry_shell$ 

First we start running this code with the argument 2 via ./retry_shell 2. Second we run our program called ./counter that we provide you. It prints Not done yet... and then the program is killed (does not exit normally). Our shell then prints retrying... and runs our ./counter again without us having to type it in. It is again killed and so our shell retries it one more time where the program is again killed. After having run it once originally and then two more retries, all of which failed to exit normally, then it prints “Failed to run program after retrying”.

It then reprompts the user who runs ./counter again. It again prints Not done yet...and then is killed. Our program retries it and this time it does exit successfully (without printing anything) so our shell reprompts and the user types in exit to exit the shell.

Lets take a look at another example that uses our program roulette.cpp. roulette.cpp is a program that simulates rolling a six-sided die. It prints out what it rolls and if it rolls a six it is considered a success and exits normally. If it doesn’t roll a six then it fails and does not exit normally.

Note: you may get a different result when you run ./roulette since it is based off of random chance.

root/workspace/retry_shell$ ./retry_shell 2
$ ./roulette
FAILED: ROLLED A 2
retrying...
FAILED: ROLLED A 4
retrying...
FAILED: ROLLED A 2
Failed to run program after retrying
$ ./roulette
FAILED: ROLLED A 1
retrying...
SUCCESS: ROLLED A 6
$ echo hello
hello
$ exit
root/workspace/retry_shell$ 

Again this program is run with the argument of 2 specifying that it will retry two times. It prompts the user and the user types ./roulette. When it is run this time it fails and aborts. Our shredder retries the program two more times which also fail and abort before giving up and reprompting the user. The user asks that it runs ./roulette again and it fails once, but succeeds and exits normally from the first retry. Since it succeeds on the first retry, a second retry is not even attempted. Instead the shell prompts the user for the next command and the user runs echo hello which is run and exits normally. The shell prompts the user again and the user types exit to exit the shell.

Instructions

Among the starter files you will find:

For retry_shell.cpp there are some specific requirements:

  1. Process any command line args. If the user passes in more than one command-line arg or the command line arg is not a positive integer, it should print an error to stderr (cerr) if the argument is invalid. (Part of the retry functionality)
  2. Your program should read in commands from stdin (e.g. the cin stream) one line at a time. A command consists of a program and the programs arguments.
  3. Continue reading and executing commands until you read EOF from stdin or exactly exit is input on one line.
  4. Wait for the current command (sequence of programs) to terminate before starting the next command.
  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).
  7. If an integer is passed in through command line argument, then if the program does not exit normally it retries that program until it exits normally or it has retried N times.

If you are not sure of whether you should do certain behaviour, look at what we do in lecture and/or feel free to ask on Ed. You can make any changes to retry_shell.cpp to implement your shell. Note that you are also free to alter the provided programs stdin_echo, counter and roulette which may be useful for debugging 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 SPECIFICATION. It shouldn’t be too long and will help with your understanding of the assignment. You can skip the retry section until you have gotten basic functionality working.
  2. Take a look at the provided program stdin_echo.cpp and the lecture & recitation examples. Make sure you understand what is happening in these programs and try running them yourself.
  3. Start implementing retry_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. you will need to modify the makefile to compile your code like you did in the past assignments.
  4. Modify your program so that if a user inputs the end of file character ctr + d or types in exactly exit and hits enter, then your program should exit gracefully.
  5. Modify your program to handle forking the user input as commands, similar to how we do in some of the lecture examples, but instead of hard-coding the command to run, we get the command in from stdin, where each line is one command and its command line arguments.
  6. Make sure you handle the case where if someone hits enter after typing no command (or only types spaces or tabs then hitting enter) that your shell reprompts
  7. Make sure your shell does not reprompt until the previous command finishes. For example, if you type sleep 3 then retry shell should not reprompt until about 3 seconds have passed.
  8. Modify your program to detect a command line arguement for the retry functionality. If the input is not a positive integer, it prints an error and exits.
  9. Implement the retry functionality.

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. Like in the past homework assignments, you will need to modify the makefile to compiler your retry_shell.cpp into the executable retry_shell. You should be able to figure this out on your own but feel free to attend office hours if you have any questions.

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 retry_shell. You can then run this by typing in ./retry_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 ./retry_shell You will need to pass in some user input once you do this and eventually type exit for valgrind to process the program.

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 retry_shell you can compare the behaviour/output of it to some of the examples shown above in the specification. Your output should be almost exactly the same as the one provided above.

You can also submit to the autograder to see your results. The tests and test files used for the autograder that weren’t provided in the original zip can be downloaded here:

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

and then unzip the retry_shell_tests to get:

You can use the tests in the tests directory to check your code locally.

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

cat ./tests/simple_input.txt | ./retry_shell > my_output.txt

and then compare the file my_output.txt to ./tests/simple_expected.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_expected.txt

You can repeat this with the other test cases that are in the tests folder.

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

Clang Format and Clang Tidy

The makefile we provided with this assignment is configured to help make sure your code is properly formatted and follows good (modern) C++ coding conventions. To do this, we make use of two tools: clang-format and clang-tidy

To make sure your code identation is nice, we have clang format. All you need to do to use this utility, is to run make format, which will run the tool and indent your code propely. Code that is turned in is expected to follow the specified style, code that is ill-formated must be fixed and re-submitted.

clang-tidy is a more complicated tool. Part of it is checking for style and readability issues but that is not all it does. Examples of readability issues include:

Not using curly braces around if statements and loops:

if (condition)  // clang-tidy will complain about missing curly braces
  cout << "hello!" << endl;

Declaring variables or parmaters with names that are too short:

void foo(char c) { // clang-tidy will complain about the name `c`
  // does something
}

Having functions that are too complex and long. The tool calculates “cognitive complexity” of your code and will complain about anything that is too complex. This means you should think about how to break your code into helpers, because if you don’t, clang-tidy will complain and you will face a deduction. More on this specific error can be found here: Cognitive Complexity

clang-tidy is also useful for noticing some memory errors and pointing out bad practices when writing C++ code. Because of all this, we are enforcing that your code does not produce any clang-tidy errors. You can run clang-tidy on your code by running: make tidy-check. Whenever you compile your code using make then it should also re-run clang-tidy to check your code for errors. Note that you will have to fix any compiler errors before clang-tidy will run (and be useful).

If you have any questions about understanding an error, please ask on Ed discussion and we will happily assist you.

Submission:

Please submit your completed Makefile and retry_shell.cpp to Gradescope