Friday, October 24, 10pm
ISO C11 with GNU extensions. GCC flag -std=gnu11
See Project 1 on Canvas for the Github Classroom link.
This is a pair assignment, but you can work alone, if you so choose.
Submit the contents of your repository via Gradescope. See Deliverables below for what to submit. If you are working with a partner, do not forget to include their name with the submission.
The first project of this class, is to write a Shell. This project is more involved than the previous assignments and requires more planning as well as programming. You are asked to develop a moderately complex piece of C code from scratch. Start early with planning, experimenting, and prototyping.
The first step is to implement basic shell functionality running a
single user-specified command at a time. The shell should display a
prompt, read the command and its arguments, and execute the command.
This should be performed in a loop, until the user requests to exit the
shell. The commands can have 0 or more arguments and these arguments may
be enclosed in double quotes ", in which case the enclosed
string is treated as a single argument. You will need a tokenizer to
split up the input into tokens you can process. You can use the one you
produced for [Assignment 4]. However, we will provide a basic tokenizer
library a few days after this project has been released.
Example interaction:
$ ./shell
Welcome to mini-shell.
shell $ whoami
ferd
shell $ ls -aF
./ .git/ shell* shell.o tokens.h vect.c vect.o
../ Makefile shell.c tokens.c tokens.o vect.h
shell $ echo this should be printed
this should be printed
shell $ echo this is; echo a new line
this is
a new line
shell $ exit
Bye bye.
Here are the requirements for the basic shell
After starting, the shell should print a welcome message:
Welcome to mini-shell.
You must have the following prompt: shell $ in front
of each command line that is entered.
The maximum size of a single line shall be at least 255 characters. Specify this number as a (global) constant.
Each command can have 0 or more arguments.
Any string enclosed in double quotes (") shall be
treated as a single argument, regardless of whether it contains spaces
or special characters.
When you launch a new child process from your shell, the child process should run in the foreground by default until it is completed. The prompt should be printed again and the shell should wait for the next line of input.
If the user enters the command exit, the shell
should print out Bye bye. and exit.
If the user presses Ctrl-D (End-Of-File, aka EOF), the shell should exit in the same manner as above. Implement this early on. Our tests rely on your shell being able to terminate on EOF.
If a command is not found, your shell should print out an error
message, [command]: command not found (replacing
“[command]” with the actual command name), and resume
execution.
For example:
shell $ dfg
dfg: command not found
shell $System commands should not need a full path specification to run in the shell.
For example, issuing ls should work the same way it
works in BASH and run the ls executable that might be
stored in /bin, /usr/bin, or elsewhere in the
system path.
Part 2 expands on the basic shell from Part 1. You are asked to implement 4 builtin commands, as well as the following 3 operators:
echo one; echo twosort < foo.txtsort foo.txt > output.txtsort foo.txt | uniqNote that these operators can be combined. Follow the implementation strategy suggested below. This will give you the relative priorities of the operators.
In addition to running programs, shells also usually provide a variety of built-in commands. Let’s implement some.
The shell should support at least the following built-in commands, in
addition to exit from Part 1:
cd (change
directory)pwd command (not a built-in).
sourceprevprev with other
commands on a command line.
help;The behavior of ; is to execute the command on the
left-hand side of the operator, and once it completes, execute the
command on the right-hand side.
For example:
```
shell $ echo Boston; echo San Francisco; echo Dallas
Boston
San Francisco
Dallas
shell $ dfg; uptime
dfg: command not found
20:04:40 up 44 days, 6:14, 60 users, load average: 2.05, 1.93, 1.70
shell $
```
<A command may be followed by < and a file name. The
command shall be run with the contents of the file replacing the
standard input.
>A command may be followed by > and a file name. The
command shall be run as normal, but the standard output should be
captured in the given file. If the file exists, its original contents
should be deleted (“truncated”) before output is written to it. If it
does not exist, it should be created automatically. You do not need to
implement output redirection for built-ins.
|The pipe operator | runs the command on the left hand
side and the command on the right-hand side simultaneously and the
standard output of the LHS command is redirected to the standard input
of the RHS command. You do not have to support piping the output of
built-ins.
Implement the shell in shell.c.
Include any .c and .h files your
implementation depends on and commit everything to your repository.
Do not include any executables, .o files,
or other binary, temporary, or hidden files; or any extra
directories.
All the functionality needs to be implemented by you, using system calls. Writing code that relies on the default shell in any form does not fulfill the requirements.
A grammar for a language specifies all the valid examples of expressions (or sentences) in that language. Our shell has the following grammar. This should help decide what is a valid command line, but also to help you structure your code. If you took Fundies, it might help to think of a grammar as a collection of related (inductive) union definitions.
A command line is one or more pipe
commands separated by a semicolon: ;.
A pipe command is one or more
redirections separated by a pipe: |.
A redirection is a simple command,
optionally followed by a > or a < and a
file name.
A simple command is either a built-in command or a program name followed by zero or more arguments.
Here’s a set of “rough and ready” guidelines for tackling the extra shell functionality. Note that each subcommand might contain other operators as well. You might want to implement sequencing or redirection first.
Sequencing: command1; command2
command1 (recursively).command2 (recursively).Note, that you may have success processing a sequence of commands using an ordinary loop too.
Pipe: command1 | command2
stdout, close other side.command1.stdin, close other side.command2.Redirection: command <OP> file
command (recursively).Here are some examples you can use to test the shell functionality.
The line
echo one; echo two
should print
one
twoRunning
echo -e "1\n2\n3\n4\n5" > numbers.txt; cat numbers.txt
should print
1
2
3
4
5
and result in a file called numbers.txt being created in
the current directory.
Running
sort -nr < numbers.txt
after the above, should print
5
4
3
2
1Running
shuf -i 1-10 | sort -n | tail -5
should print
6
7
8
9
10You might consider some of the following optional features in your shell to challenge yourself (there is no extra credit for this):
Switching processes between foreground and background
(fg and bg commands).
Grouping command expressions. E.g.:
( cat prologue.txt ; ( cat names.txt | sort ) ; cat epilogue.txt ) | nlAs before, we provide you with a Makefile for convenience. It contains the following targets:
make all – compile everythingmake shell – compile the shellmake shell-tests – run a few tests against the
shellmake test – compile and run all the testsmake clean – perform a minimal clean-up of the source
treeIf you get unexpected results (such as extra output) when
implementing redirections, try using fflush(stdout)
before closing/replacing standard output. Look at
fflush’s manpage.
Use the provided unit tests as a minimum sanity check for your implementation. Especially before the autograder becomes available.
Write your own (unit) tests. Doing so will save you time in the long run, especially in conjunction with the debugger. In office hours, the instructors or the TAs may ask you to show how you tested code that fails.
Follow good coding practices. Make sure your function prototypes (signatures) are correct and always provide purpose statements. Add comments where appropriate to document your thinking, although strive to write self-documenting code. Pick meaningful names for your functions and variables. The larger the scope of the variable, the expressive the variable name should be.
Think about and design your program in a top-down manner and split code into short functions. Leverage your knowledge of program design from previous classes.
Avoid producing “spaghetti
code”. A mutli-branch if-else if-else or a multi-case
switch should
be the only reason to go beyond 30-40 lines per function. Even so, the
body of each branch/case should be at most 3-5 lines long.
Use valgrind with --leak-check=full to
check you are managing memory properly.
A string vector implementation might be useful.
Avoid printing extra lines (empty or non-empty) beyond what is required above. This goes both for the tokenizer and the shell. Extra output will most likely confuse our tests and give false negatives.
man is your friend. Check out fork,
open, close, read,
write, dup, pipe,
exec, …