CS 475, Spring 2018, Homework 1, Due Feb 7, 2pm
(Added text in the assignment is highlighted in red)
- 2/5: Added notes pointing out to call chdir() in order to implement cd, and a pointer to the exec man page.
- 1/31: Updated handout (and autolab) to fix inputs/p1-arg output (was accidentally generated on a mac and had extra spaces)
- 1/29: Correct the total points to add to 100%. P1: 45%, P2: 45%, Crosscutting: 10% (crosscutting was previously labeled both 15 and 20% in different places)
- 1/28: Strike the phrase “(including external commands and built in commands, including calling history itself)” from the first bullet of 2.3
- 1/28: In handout and on Autolab: revise inputs/p1-long-line and inputs/p2-history-clear to remove the dangling spaces; revise expected output files as well
- 1/26: Add note about fflush in part 1
Start by downloading the handout:
Please post questions on Piazza
The purpose of this assignment is to become more familiar with the concepts of process control. You’ll do this by writing a simple Unix shell program that supports job control.
The shell is just another program installed on your computer. When you open a new shell, the system automatically invokes your shell program, then displays that program. On most linux or BSD-based systems (like Mac OS X), this default shell is bash – but there are many other shells, too.
A shell is an interactive command-line interpreter that runs programs on behalf of the user. A shell repeatedly prints a prompt, waits for a command line on stdin, and then carries out some action, as directed by the contents of the command line.
The command line is a sequence of ASCII text words delimited by whitespace. The first word in the command line is either the name of a built-in command or the pathname of an executable file. The remaining words are command-line arguments. If the first word is a built-in command, the shell immediately executes the command in the current process. Otherwise, the word is assumed to be the pathname of an executable program. In this case, the shell forks a child process, then loads and runs the program in the context of the child.
Your task will be to implement a simple shell, called cs475sh.
Your shell should be implemented entirely in the shell.c file provided in the handout. It should compile with the provided Makefile; do not change the Makefile. Your code should compile without any compiler warnings.
Your shell must check the return value of all system functions that you call. Errors must be printed in exactly the following standard format:
If errno is set, fprintf(stderr, "error: %s\n", strerror(errno));.
If errno is not set, fprintf(stderr, "error: %s\n", yourCustomErrorMessage);.
Your shell will be automatically graded for correctness (note that there will be a manual grading phase to make sure that your error handling is correct, and to check other hard-to-automatically-check issues). Included with your handout is a simple version of the (large) test script that we will use to grade your assignment. Upon submitting your assignment, our server will automatically compile and test your assignment and provide you with test results. You can resubmit an unlimited number of times until the deadline.
Note: Your code must compile and run in a linux environment (e.g. the ITE lab, and the autograder). In particular, you may be tempted (on a Mac) to use the fgetln function. This function is not part of standard C, and hence will not work on linux.
Part 1: The Simple Shell (45%)
Your shell should consist of a command loop, reading a line from stdin (one line at a time), then executing the command requested.
Note the following requirements:
- Your shell must run continuously, and display a simple prompt when it’s waiting for input, The prompt should be EXACTLY
475$ No spaces, no extra characters, etc.
- You should call fflush(stdout); right after printing the prompt – otherwise it may not appear on the console. (Thanks to Aaron M for suggesting this on Piazza)
- On the input EOF (ctrl-d), your shell should cleanly exit (cleaning up any allocated resources)
- Your shell should read a single line from
stdin and parse it (and all of its arguments)
- The delimiter between a command and its arguments (and between multiple arguments) is the same: a whitespace (ASCII character 32)
- There is no need to handle special characters in any special way (like quotation marks, backslashes, etc). That is, the command 475$/bin/ls "/home/jon/My Documents/" would parse to command: /bin/ls argument 1: "/home/jon/My argument 2: Documents/"You must not set a limit on the length of the input line.
- You should parse only at most 127 arguments (plus the command string itself). If there are more than 127 arguments, then your shell should print the error error: too many arguments.
- After you parse the command, execute it. There will be two kinds of commands: (1) a reference to an executable (as in the case of /bin/ls above), or (2) a built in command. For part 1, you should only consider calling external executable files. You must execute external commands with fork() and exec(), NOT with system(), popen(), etc. Refer to the documentation for exec and its variants, most students will find execvp() makes the most sense for them.
Part 2: Implementing built in commands: exit, cd, history (45%)
In addition to invoking external commands, all shells provide some number of built-in commands that trigger special behaviors in the shell. You will implement three built in commands for cs475sh:
- exit – Exit is a built-in command that takes no arguments. When invoked, it will perform any cleanup necessary (e.g. freeing allocated memory and other resources) and then exit your shell.
- cd [dir] – Short for “change directory,” cd will change the current working directory. For cs475sh, cd takes exactly one argument: the directory to change to (other shells often provide much more complex behaviors). You should implement this by directly calling the chdir command.
history [-c] [offset] – Similar to the bash
history command, but much simpler. Note: before any command is run, it is added to the history list.
- Running history without any arguments displays the last 100 commands that were executed, along with their arguments. Each entry in the history record is prefixed with the index of the command in the list (with the oldest value having index 0, then increasing). Valid indices, then, are 0-99, inclusive. Once 100 commands are executed, the oldest entry (0) is removed from the list, and each item gets shifted up one. You should include all commands that were executed (regardless of whether they passed or failed); you should include cd; you should not include blank lines (e.g. if the user enters a blank line then hits return, do not include the blank line in the history); you should not include calling history itself.
- history -c will clear the entire history list, removing all entries. history -c should not be recorded in the history (e.g. it runs, and then clears the history)
- history [offset] will automatically re-execute that command (and record the execution in the history log).
Add error handling for all your built-in commands: consider for instance, cd’ing to an invalid directory, or re-executing an invalid item from the history list.
Non-functional aspects (10%)
10% of the homework grade will be based on cross-cutting aspects:
- Your code must compile with no warnings when using the -Wall flag to GCC, as the Makefile does)
- You should include reasonable documentation (comments)
- Your code should be evenly formatted (we won’t enforce that you use a particular number of spaces to indent, or line-length, as long as it’s consistent)
Your shell will be graded on a series of functional tests, making sure that it implements the specification above. Your shell is expected to correctly allocate and free memory; we will test your shell using valgrind. Your shell is also expected to handle ALL error conditions from system functions (including but not limited to memory allocation and process creation). We have provided a (not exhaustive) test suite which should help you judge (on your own) how well you have done with the functional requirements. We may add additional tests beyond what we are providing you with now, so please do not rely entirely on them (in particular, the automated tests do not verify that you check all error codes).
Hand In Instructions
You must turn in your assignment using Autolab (You MUST be on the campus network, or connected to the GMU VPN to connect to Autolab). If you did not receive a confirmation email from Autolab to set a password, enter your @gmu.edu (NOT @masonlive) email, and click “forgot password” to get a new password.
Upload ONLY your shell.c file. When you upload your assignment, Autolab will automatically compile and test it. You should verify that the result that Autolab generates is what you expect. Your code is built and tested in a Linux VM. Assignments that do not compile using our Makefile will receive a maximum of 50%. Note that we have provided ample resources for you to verify that our view of your assignment is the same as your own: you will see the result of the compilation and test execution for your assignment when you submit it.
You can resubmit your assignment an unlimited number of times before the deadline. Note the course late-submission policy: assignments will be accepted up until 24 hours past the deadline at a penalty of 10%; after 24 hours, no late assignments will be accepted, no exceptions.
Understanding the Autolab output
You MUST be on the campus network, or connected to GMU VPN to connect to Autolab.
Autolab will give you a score from 0-26. This score will help you understand how well you have done implementing the assignment, but you should realize that it does not directly map to a grade on the assignment (for one thing, the test cases don’t map to the weightings provided above). First, you will notice that you can receive 0-2 points for “compiles” – 1 point if “make” succeeds, and 2 points if “make” succeeds with no warnings. We’ve provided you with 13 test cases. You’ll receive 1 point for each if they succeed (the output matches the exact output that we expect), and 2 points if they succeed and valgrind shows no memory leaks. You can see the exact inputs that we are feeding your shell in the handout – inputs in the inputs directory, and the exact outputs expected in the outputs directory. You can run the driver yourself if you wish, to see the outputs without needing to run on our system.
Please post questions on Piazza