Goals
- Practice with writing a self contained C++ program that reads from stdin
- Get you thinking about and using concurrent processes effectively
- Exposure to how Unix based operating systems work on the command line
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:
- First line
root/workspace/retry_shell$ ./retry_shell
is us starting ourretry_shell
. The following lines are what happens when we run retry shell. - Second line
$ echo hello
. First the program is prompting us with$
and then the user types inecho hello
and hits enter. - The third line is the output of running the program
echo hello
- 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) - The fifth line starts with a reprompt
$
and then this time the user types insleep 3
and hits enter. At this point the programsleep 3
is run in the terminal. This program doesn’t print anything, just waits for 3 seconds. - The sixth line starts with a prompt
$
and the user typing inecho I am doing well!
before hitting enter - The seventh line is the output of running the command
echo I am doing well!
from the previous line. - 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 runningretry_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
You should implement the basic functionality first before implementing the retry feature. If you are still working on the basic functionality it may be useful to move on to the sections below.
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:
-
retry_shell.cpp
an empty file for you which is where you will implement your UNIX shell. -
Makefile
used to compile your program. You will have to modify this makefile like you have in past assignments to compile yourretry_shell.cpp
into the executableretry_shell
. -
stdin_echo.cpp
a simple program that reads from stdin, and prints everything it reads to stdout until EOF is read in, and then it terminates. This program is implemented for you and you may be useful for examples of reading student input. -
counter.cpp
is a program that always dies (does not exit normally) the first 4 times you run it. On the fifth time it will exit normally. Note that it “remembers” how many times it was run even if we run it again after exiting fromretry_shell
. -
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.
For retry_shell.cpp
there are some specific requirements:
- 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) - Your program should read in commands from
stdin
(e.g. thecin
stream) one line at a time. A command consists of a program and the programs arguments. - Continue reading and executing commands until you read EOF from stdin or exactly
exit
is input on one line. - Wait for the current command (sequence of programs) to terminate before starting the next command.
- 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. - Programs can be named by either an absolute path or just by the program name (
execvp
should handle this for you). - 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.
We HIGHLY recommend that you read the instructions, basic features and suggested approach, and are familiar with lectures that covered this material before you start writing any code for this homeework.
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.
- 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.
- 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. - 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. - Modify your program so that if a user inputs the end of file character
ctr + d
or types in exactlyexit
and hits enter, then your program should exit gracefully. - 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.
- 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
- 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. - 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.
- Implement the retry functionality.
Hints
- We highly suggest reading through the specification before starting and reviewing the processes lecture given in class
- You may want to implement your own split function to properly parse user input into an vector of strings that you can use for executing. You can take inspiration from what you did in HW02. You likely want to split on tabs, newlines and whitespace characters:
" \t\n"
. - Take inspiration from the provided sample programs and the programs provided in class relating to
fork
, andexec
. - Regularly test your code on
valgrind
to make sure you don’t have any memory errors, as this can cause problems in your code. - You will almost certainly want to make use of the
execvp()
,fork()
, andwaitpid()
functions in your implementation. - you may find the
fork_example.cpp
,exit_status.cpp
andwait_example.cpp
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
-
iostream
cin
cout
cerr
endl
-
string
getline
string
stoi
vector
-
stdexcept
exception
invalid_argument
out_of_range
C System Calls
-
unistd.h
fork
execvp
-
sys/wait.h
wait
waitpid
WIFEXITED
sys/types.h
You should not use any C-style functions when C++ variants exist. For example, you should not use strtok
or printf
when C++ has appropriate alternatives.
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:
-
nap.cpp
a simple C++ program that is compiled and used to test your shell -
test_files
a folder some sample txt files that we have your shell interact with as part of the tests -
tests
a folder that contains most of the tests the autograder uses.
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).
Code that has clang-format
, clang-tidy
or any compiler errors will face a deduction.
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