Programming Assignment 3

Simple Shell

Objectives

The objectives of this project are to learn how a Unix shell works, to write a simple shell, to create processes, to handle signals, to use Unix system calls and C library function calls, and to become a better programmer.

Background readings

The Assignment

To write a simple shell in C with the following "built-in" commands.

Overview

A shell has a loop that prints a prompt, consisting of a "prefix," which is initially an empty string, followed by ' [cwd]> ', where cwd is the "current working directory," and executes commands until the exit command is entered. In the loop, the shell checks if the command is one of the built-in commands (given below) or an executable (external command) in the search path. The shell needs to pass arguments to the external commands (i.e., build an argv array). If the command entered is neither one of the built-in commands nor in the search paths, the shell prints "command: Command not found." where command is the command name that was entered.

More Details

  1. Obviously, the shell needs to parse the command line entered. A good way to do this would be to use fgets(3) to read in the (entire) command line, then use strtok(3) with a space as the delimiter. The first "word" will be the command (where we'll ignore the possibility that a command could have a space in its name). All the "words" after that are arguments to be passed to the command (which the shell needs to put into a char**). Check out parse-cmd.c to see how this is done. fgets() returns NULL on error or when end of file (EOF) occurs while no characters have been read.
  2. After you get the command, check if it is one of the built-in commands (explained below). If it is, then perform the corresponding function for that.
  3. If it is not one of the built-in commands, check if it is an absolute path (a command begins with '/') or a path begins with './', '../', etc., and run that if it is an executable (use access(2) to check).
  4. If the command is neither of the above cases then search for the command in the search path by looping through the paths stored as a linked list as given in get_path.c in the skeleton code. (You may gcc get_path.c get_path_main.c and run the generated a.out to see what happends.) You may also use your own linked list code. Use access(2) in a loop to find the first executable in the paths for the command. snprintf(3) would be useful here (as using strcat() has caused problems for some people).
  5. Once you find the (external) command, you should execute it by ullingsing execve(2). Review execve_ls.c to see how execve(2) should be invoked. The shell should also execute waitpid(2) and print out the return status of the command if it is nonzero like tcsh does when the printexitvalue shell variable is set. Look into using the WEXITSTATUS macro from <sys/wait.h>.
  6. Before executing a command, the shell should also print out what it is about to execute. (i.e. print "Executing [pathname]"; for built-in commands, print "Executing built-in [built-in command name]").
  7. The shell needs to support the * wildcard character when a single * is given. You do not need to handle the situation when * is given with a / in the string (i.e., /usr/include/*.h). This should work just like it does in csh/tcsh when noglob is not set. The shell need only to support the possibility of one * on the command line, but it could have characters prepended and/or appended. (That is, ls * should work correctly as should ls *.c, ls s*, ls p*.txt, etc.) Hint: implement the list built-in command explained below before attempting this. You may use glob(3) or wordexp(3)if you wish. [Review glob.c in sample code.] Note that it is the shell's responsibility to expand wildcards to matching (file) names. (Review wildcard-1.c in sample code.) If there were no matches, the "original" arguments are passed to execve(). The shell would only need to make the wildcard to work with external commands.
  8. When a user presses Control-C (Ctrl-C), the SIGINT signal is sent to all processes in the foreground process group. SIGINT should be caught and ignored by the shell to prompt for commands, but terminates the running child process otherwise. Use signal(2) and/or sigset(3) for this. Ctrl-Z (SIGTSTP) and SIGTERM should be ignored using sigignore(3) or signal(2). Check out sample code sleep_Z.c. Note that when a user is running a command (executable) inside the shell and presses control-C, signal SIGINT is sent to both the shell process and the running command process (i.e., all the processes in the foreground process group). (Review Sections 9.4 (Process Groups), 9.5 (Sessions), and 9.6 (Controlling Terminal) of Stevens and Rago's APUE book for details.)
  9. The shell should treat Ctrl-D and the EOF char in a similar way csh/tcsh do when the ignoreeof tcsh shell variable is set, i.e., ignore it, instead of exiting or seg-faulting. Note that Ctrl-D is not a signal, but the EOF char. If ignoreeof is set to the empty string or '0' and the input device is a terminal, the end-of-file command (usually generated by the user by typing '^D' on an empty line) causes the shell to print 'Use "exit" to leave tcsh.' instead of exiting. This prevents the shell from accidentally being killed. If set to a number n, the shell ignores n - 1 consecutive end-of-files and exits on the nth. If unset, '1' is used, i.e. the shell exits on a single '^D'. Review all the shell variables of tcsh. (Please review the difference between Shell Variables and Environment Variables.) You may also review this code to see how fgets() works with EOF.
  10. Your code should do proper error checking. Again, check man pages for error conditions, and call perror(3) as needed. Also avoid memory leaks by calling free(3) as needed.

Built-in Commands to support

How to get started

It is recommendated to first get the loop working to find a command, i.e., implement which first. Then you will be able to create a new process with fork(2) and use execve(2) in the child process and waitpid(2) in the parent. Then handle arguments and do the other built-ins. Remember to the read man pages for system and library calls, include the corresponding header files.

A skeleton code to get started with is here. Example code of using fork(2) and exec(2) can be found here.

Some more library functions that may be helpful

atoi(3), fprintf(3), index(3), calloc(3), malloc(3), memcpy(3), memset(3), getcwd(3), strncmp(3), strlen(3).

About which and where

The best way to learn how the which and where commands work is to use the following 3 programs (that are part of the Shell skeleton code) to create an executable as follows.
get_path.c    get_path.h    get_path_main.c
$ gcc get_path.c get_path_main.c
$ ./a.out
In tcsh, you can append a directory called /usr/local/bin to the END of search path by entering the following command and then print the new value.
$ set path = ($path /usr/local/bin)
$ echo $path
To add the directory to the FRONT of search path, do
$ set path = (/usr/local/bin $path)
In bash, to add the directory to the END of search path, do
$ export PATH=$PATH:/usr/local/bin
To add to the FRONT of path, do
$ export PATH=/usr/local/bin:$PATH
Both which and where will start searching for the target executable from the front of the path/PATH value.

Test Runs

Test your shell by running the following commands in it (in order):
[return]
Ctrl-D
Ctrl-Z
Cotrl-C
which					; test which
which ls
ls					; execute it
[return]
Ctrl-D					; make sure still work
Ctrl-Z
Ctrl-C
ls -l					; test passing arguments
ls -l -a /proc/tty
ls -l -F /proc/tty
ls -l -F /etc/perl
ls -l -a /etc/perl
where					; test where
where ls
/bin/ls -l -g			; test absolutes and passing args
/bin/ls -l
file *					; test out * wildcard
ls *
ls *.c
ls -l sh.*
ls -l s*.c
ls -l s*h.c

blah					; try a command that doesn't exist
/blah					; an absolute path that doesn't exist
ls -l /blah
/usr/bin				; try to execute a directory
/bin/ls -la /
file /bin/ls /bin/rm
which ls rm				; test multiple args
where ls rm
list					; test list
list / /usr/bin
cd 					; cd to home
pwd
cd /blah				; non-existant
cd /usr/bin /usr/ucb			; too many args
cd -					; should go back to project dir
pwd
more sh.c   (and give a Crtl-C)		; more should exit
cd /usr/bin
pwd
./ls /					; test more absolutes
../bin/ls /
pid					; get pid for later use
kill
kill pid-of-shell			; test default
kill -1 pid-of-shell			; test sending a signal, should kill
					; the shell, so restart a new one
prompt	    (and enter some prompt prefix)
prompt 361shell
printenv PATH
printenv
setenv
setenv TEST
printenv TEST
setenv TEST testing
printenv TEST
setenv TEST testing more
setenv HOME /
cd
pwd
exit

Notes


Grading

Turn In

You need to tar up your source code, test run script file, etc. To do this, do the following.

To verify that your files are in the tar file take a look at the table of contents of the tar file like:
tar tvf YourName_proj3.tar