This section describes some very basic features of the Unix shell. The actual exercise description starts in the next section.
Read the man pages of the following utilities: ls, cat, who, ps and echo. All allow you to display some information on the screen. The output of these programs doesn't have to be sent to the screen right away: it can be filtered several times. The most common Unix filters are head, tail, sort, wc, grep, tr, tee and uniq. Take a quick look at their man pages as well. Filters work as follows: Given a file or a list of files as parameters, they will process these files; given no parameters, they will process the standard input. This convention gives great flexibility for combining filters through redirections and pipes:
ls a* | wc -l
who | sort > users
who | grep mary
cat < index.html | tr -sc A-Za-z
'\012' | sort | uniq -c | sort -n | tail
Another nice things the shell gives us is wildcards:
lpr *
ls c?.doc
cat data[127]
A range of characters can also be specified. To delete all files that start with a
letter: rm [A-Za-z]*
A third nice thing the shell gives us is variables. We define a variable in a command of the form "var=value", and from then on the string $var in commands will be replaced by the string "value". For example, the following series of commands will change the directory to /a/b/c/d:
base =
/a/b
below = $base/c
chdir $below/d
The shell parses every command it reads to identiry redirections, pipes, wildcards and variables. The whole process is transparent to the programs themselves - sort, for example, doesn't know whether its input really comes from the standard input or from a file, doesn't care whether its output really goes to the screen, and will never know if the list of input files we sent it originally contained a wildcard.
Write a program called 'hshell', that acts as a simple command line interpreter. The program should display a simple prompt in which users can enter commands, and then parse and execute these commands. The following command formats must be supported:
program [optional arguments]
program [optionals] <
in_file
program [optionals] >
out_file
(creates or truncates out_file) and program [optionals] >>
out_file
(creates or appends to out_file).program [optionals] | program [optionals]
| ... | program [optionals]
cat < in_file |
fgrep a | sort > out_file
.program
[optionals] &
The hshell program reads commands from the standard input, just like your usual Unix shell does. For commands that run in the foreground, the shell waits for the command's completion before starting the next command. When running commands in the background (i.e. "emacs &"), the shell does not wait for the command to finish but instead returns to read the next command right away. However, when a process that was running in the background terminates, the shell should print a line to the standard error containing the word "Done" and the terminated process id.
Whenever an error is encountered, print a detailed error message to the standard error (not standard output!), and return to wait for the next command. The hshell process ends only when it encounters the end-of-file character (type Ctrl+D at the keyboard for it).
Make sure that commands that change the shell process's
state work as well. Specifically, make sure that the chdir
and umask
commands work as usual.
Test your program with the redirection and piping examples
from class and the previous section, and other commands as complex as you can think of.
You are encouraged to share tests (only!!) with other students. Note, however, that some
combinations of redirections and pipes are not legal, for example: "ls >
outfile | sort
" or "ls | sort < infile
".
Testing gets easier if you remember that the input and output of the shell itself can be
redirected: You can write a series of tests in a file called "my_tests
"
and then run "hshell < my_tests
".
Use any parsing method that you like in your program. Note that the use of white characters (spaces and tabs) is optional but not required around special characters, so "ls >a &" and "ls > a& and "ls >a&" are identical.
There is one ten points bonus offered for this exercise. You have to implement shell variables: These are created by typing commands like "dir=/usr/include/sys" and then using them in other commands like "ls $dir/*.h". The dollar sign is a special character that signifies the beginning of a variable name. You should look for such variables in commands, and replace them with the values the variables hold.
Each process has a list of variables associated with it - its "environment table". This table is a list of strings of the form "Name=Value", where 'Name' is the name of the variable. This list is inherited from parent to child processes, therefore you can't implement it as an internal linked list in your program. You must use the system's list of variables instead. In order to do so, you'll have to learn the following functions: putenv, getenv, setenv and unsetenv. See the man pages for all of them.
Having implemented shell variables in this way, your shell
program will already have several variables defined every time it starts running. These
will be variables it inherits from its own parent process, such as PATH, HOME and others.
Commands like "echo $HOME
" and "PATH=$PATH:/usr/include
"
are examples of simple tests. Also, make sure that your shell can inherit new variables it
defines to its own child processes.
This bonus requires you to learn a few functions on your own, but if done smartly is quite easy.
Your submission must include a README, a Makefile, and the source code of the programs. The Makefile should create eaxctly one executable called 'hshell'. You may include as many source files as you wish. To submit, send an email to the bodek, containing the full names, id numbers and logins of the submitting pair, and attach all the relevant files. This time, in contrast to previous exercises, the files you send will be checked (not the ones in your account), and the time of submittion (for late submission penalties) will be the time of sending the mail.
Your README file should include a vivid description of how your program does what it's supposed to. If you choose to implement the bonus, include an exact description of how you did so in the README. You might lose some of the bonus points for not being able to describe your accomplishment well.
VERY IMPORTANT: You cannot use any of the following functions: system(), popen(), pclose() or exececuting the Unix shell as in execve("/bin/sh", ...). Using any of these functions or similar tricks will result in an automatic grade of zero. You are expected to create child processes using fork(), manage redirection using open() and dup2(), and handle child termination signals using signal() and either wait() or waitpid().
Good luck!