Lab: A Simple Shell
CSC 213 - Operating Systems and Parallel Algorithms - Weinman
- Summary:
- You will create your own interactive shell program.
- Assigned:
- Tuesday 9 September
- Due:
- 11:30 PM Monday 15 September
- Objectives:
-
- Learn to use basic system calls for creating and managing processes.
- Practice reading and debugging example code, and using it to solve
a different problem.
- Understand how a simple Unix shell interpreter actually works by building
one yourself.
- Collaboration:
- You will complete this lab in teams
as assigned by the instructor.
Background
The Unix shell is a familiar mechanism for interacting with a computer.
While modern Unix shells include extensive capabilities, a simple
shell facilitates two types of interactions:
- setting or modifying environment variables, such as the current working
directory or the search path, and
- locating and executing programs.
You should be familiar with the user's view of the shell from section
5.4 in your text, and from your experience using it in CSC 161 and
this class. In this lab, you will get a taste of the system programmer's
view of the shell.
Exercises
Preliminaries
Do this laboratory on any MathLAN workstation. Copy the starter
files to somewhere in your home directory.
-
$ cp -R ~weinman/public_html/courses/CSC213/2014F/labs/code/shell ~/somewhere/
You will use these files in the exercises below.
Part A: Exploring the launcher
launch.c is a program that, given the name of a file, executes
all of the commands listed in that file. This program does many of
the things your shell will need to do, but not all of them.
Examine the commands listed in the launch_set file, and
try executing them yourself (i.e., one by one in bash) to
see what the results should be.
Then compile and run the program from the shell:
-
$ make
$ ./launch launch_set
Kind of confusing, isn't it?
Read the source code and answer the following questions, which indicates
one bug for you to fix in launch.c. For the following questions,
keep your answers concise and to the point; 2-3 sentences should suffice.
- MAX_LINE_LEN and MAX_ARGS are defined, but there
is no comparable definition for the maximum length of a single argument
string. Is that ok? Why or why not? (Hint: See the manpage for strsep(3).)
- Try adding a blank line to the launch_set file and running
the launcher. Something bad will happen. Explain and then fix this
bug. (When fixed, all of the commands in the file should be executed
without error.)
You can test your bug fix using the command
-
$ ./launch -test all
- Explain what the return value of fork()
means and how this program uses it.
- What would happen if this program did not use the fork
function, but just called execv directly? (You can
test your hypothesis: Try changing line 100 from 'pid = fork();'
to 'pid = 0;')
- What is the role of the status variable?
-
Run
-
$ ./launch launch_set
several times. You should see some variation in the output. What is
happening? Why?
- Try adding a nonexistent command (e.g., fasdfjaskf or argelfraster)
to the launch set. What happens? Why?
-
Try adding the command 'cd ..' to the launch_set
file. What happens? Why? (Hint: Learn what the which(1)
command does; then try typing 'which cd' in your bash
shell at the terminal.)
- Given what you learned in A.6, is the behavior
you saw in A.8 appropriate? Is the cd command
useful in the launch program? Why or why not?
Part B: Implementing the simplest shell
Based on what you have learned from reading launch.c, implement
a simple shell. The file shell.c provides a skeleton file
in which to write your program. Your program should:
- print a prompt,
- wait for a command from the user,
- parse the command,
- execute the command in a child process,
- wait for the child process to exit,
- print a new prompt, and so on.
Here are some more detailed requirements.
- Command-line arguments:
- The simplest version of the shell should
only accept the optional -test command-line argument, as
given in the skeleton file and shown below.
- The prompt:
- The prompt should be of the form 'Shell(pid=XXXX)>
', where XXXX is the shell's process id.
- Parsing and executing the command:
- Parse the command and
execute it in a child process, as in your modified launch.c.
Your program should not crash no matter what the user types (including
a blank line).
- Waiting:
- The shell should not print a new prompt or accept any
more commands until the last command has finished executing. Commands
should execute strictly in the order they are given.
- Exiting:
- The shell should exit when the user types the 'exit'
command or when the user presses Control-D to signify the
end of the input (EOF).
You can test your code using
-
./shell -test shell-b
Here is an example run:
-
nelson$ ./shell
Shell(pid=8604)> /bin/ls
Acknowledgments.txt launch_set Makefile shell_tests.h testrunner.o
launch_seta shell shell_tests.o
launch launch_tests.c shell.c smp1.in
launch.c launch_tests.h shell.o testrunner.c
launch.o launch_tests.o shell_tests.c testrunner.h
Shell(pid=8604)> /bin/echo hello world!
hello world!
Shell(pid=8604)> /bin/pwd
/home/davisjan/csc/213/sandbox/simple_shell
Shell(pid=8604)> ./shell
Shell(pid=8609)> /bin/pwd
/home/davisjan/csc/213/sandbox/simple_shell
Shell(pid=8609)> exit
Shell(pid=8604)>
Shell(pid=8604)>
Shell(pid=8604)> exit
nelson$
Part C: Enhancing the shell
Once you have implemented the simplest shell, enhance it in the following
ways.
- The launch program requires users to give the full path for programs
to execute as commands, such as '/bin/ls'. If you've followed
the approach suggested above, then so does your shell. Modify your
shell so that users can type shorter command names, such as 'ls',
for programs that can be found through the PATH environment
variable. (Hint: Read the manpage for execv(3)
to learn about other variants on this system call.)
(Test: ./shell -test path)
- Implement the cd command, so that the user can type commands
such as 'cd ..' to change the shell's working directory.
Be sure to print an error message to stderr if changing the
directory fails. (Hint: This must be implemented as a built-in command
evaluated in the parent process, rather than forking a child process.
Do you see why?)
Handle the arguments to cd as follows:
- If given 1 argument (e.g., 'cd foo'), use chdir
to change the current directory to the given path. (Test: ./shell
-test cd)
- If given no arguments ('cd'), use getenv(3)
to find the value of the HOME environment variable, and change
to the home directory. (Test: ./shell -test cdhome)
- If given 2 or more arguments (e.g., 'cd foo bar'), print
the error message cd: Too many arguments
to stderr.
- Modify the shell so that it uses waitpid rather than wait
to wait for the most recent child process to exit. If the child process
terminates with a status other than EXIT_SUCCESS, the shell
should print to stderr a message of the form 'shell: Process
%1 exited with status %2', where %1 is the pid and %2
is the status. (Test: ./shell -test error)
Here is an example run with these enhancements:
-
nelson$ ./shell
Shell(pid=13365)> pwd
/home/davisjan/csc/213/2010F
Shell(pid=13365)> whoami
davisjan
Shell(pid=13365)> which whoami
/usr/bin/whoami
Shell(pid=13365)> cd /usr/bin
Shell(pid=13365)> pwd
/usr/bin
Shell(pid=13365)> cd
Shell(pid=13365)> pwd
/home/davisjan
Shell(pid=13365)> pwd -q
pwd: invalid option -- q
Try `pwd --help' for more information.
shell: Process 13372 exited with status 256
Shell(pid=13365)> exit
nelson$
Note: As in Lab 1, you should remove or disable (e.g., with #ifdef)
any additional debugging output you may have created before running
the tests or submitting your lab.
What to turn in
- Your fixed launch.c
- Your complete shell.c
- A single pdf containing (merged)
- Answers to the questions in Part A
- Your enscripted launch.c and shell.c
- Transcript of a compilation and testing of launch
-
make clean launch
./launch -test all
- Transcript of a compilation and testing of shell
-
make clean shell
./shell -test all
Extra Credit
- Many shells, including bash, have a useful feature that allows
the user to go back to the directory they were in before the last
cd command. The user invokes this feature by typing 'cd
-'. Try 'cd -' in bash several times to understand
how the feature works, then implement it in your shell.
- Modify your shell so that it can be used to execute scripts. It should
optionally take the name of a script file as a command-line argument.
If this argument is present, then it should read and execute commands
from the file. It should not print the prompt. (This behavior will
be similar to the launch program, except that commands will
be executed in order.)
- Be creative. Identify another shell feature that you can figure out
how to implement, and implement it. Be sure to explain the feature
and your implementation.
Acknowledgment:
This lab was adapted by Janet Davis with portions from several sources:
- Lab Exercise 2.1, A Simple Shell, in
Gary Nutt's Operating Systems (3/e)
- Assignments given previously in CSC 213 by Henry Walker and Janet
Davis;
- Operating system programming problems by Lawrence Angrave at the University
of Illinois at Champaign-Urbana (UIUC).