Shell We Run?
The Unix shell program is an ordinary user-mode
program which reads lines of text, breaks
them into words, and runs the result.
The first word is treated as
the name of a command, and the list of words becomes the array of strings
passed to main as argc and argv.
This assignment is to create a simple shell.
When working, your program would run something like this:
[tom@tomslap minishell]$ ./pgm
cmd> ls
Makefile boom.cpp die1 lbtest.o pgm startmk~
base boom.cpp~ die1.cpp linebreaker.cpp pgm.cpp
base.cpp complete lbtest linebreaker.h pgm.cpp~
base.o complete.cpp lbtest.cpp linebreaker.o pgm.o
boom complete.o lbtest.cpp~ mini1b.cpp startmk
cmd> ls -l pgm pgm.cpp
-rwxrwxr-x. 1 tom tom 26648 Sep 17 22:52 pgm
-rw-rw-r--. 1 tom tom 149 Sep 17 22:22 pgm.cpp
cmd> ls Makefile missing
ls: cannot access 'missing': No such file or directory
Makefile
* Process 57845 exited with code 2
cmd> ./boom
* Process 57846 crashed: Segmentation fault
cmd> exit
[tom@tomslap minishell]$
Your program will execute a simple loop, whose body does this:
- Issues a prompt and read one line of text.
- Break that line into words.
- If the command is a built-in, the program handles it, then
reads the next line. Otherwise,
- Executes a fork to create a new process. Report the cause
of any failure (and, of course, don't exec if the fork failed).
- The child process treats the first word in the list as the name of a
command and runs it using an exec call. It sends the whole list as
the parameters to the program. Report the cause of any failure.
- The parent process waits for the child process to exit, then reports its
status.
- Back to the read for the next command.
Your program will have to read a line of input and break it into words.
You are given a C++ LineBreaker
class
to help with that, and
an example program that shows how to use it.
A LineBreaker object is constructed from a string, then behaves as an
array of the words in that string.
The execution example from the notes
shows how to use fork() and exec()
to run another program. The LineBreaker also shows how to send the
resulting array of strings to exec, which the runner example does not.
You will want to swipe code from both these examples.
Here's what you need to do. You may want to swipe code from both the
fork/exec example, and from the
line breaker example.
- Create a while
loop to repeatedly issue a prompt, then
read a line and execute it as a command.
- After reading a line,
break it into words using a LineBreaker object.
If there are no words (the input line was all spaces), simply go on
to read another line. (A continue might be just the ticket here.)
- After breaking into words, check for a built-in command.
You are to implement two commands that cannot be run using
fork/exec. These are exit and
cd, which must be executed by the shell itself.
See below. After execution of the built-in, return to the top
of the loop to read the next command.
- If you get past all of that, you have an external command to run.
Perform the fork() and
exec(), much as the example does.
If the fork succeeds, the child process performs the
exec(), and the parent will wait for the child to finish.
- There are several forms of exec under Unix. You want
execvp.
This version takes the argument strings in an array, and also
searches for command files in the usual places, so you don't have
to type the full path of the program you want to execute.
The LineBreaker example shows how to make this call.
- Check the return code from each of the fork, exec, or wait calls to
check for failure. (Actually, exec has failed if it returns at all.)
You will want
to use errno and
strerror to
print a nice message giving the cause of the failure. The
LineBreaker example has a use of this as well.
- After the wait() returns in the parent process, you should
check the exit status and report on the success of the program you
ran. if it exits with a code other than 0
(an error exit), or if it crashes, report that. For normal exit,
say nothing. Report the exit code, or the reason for the crash.
More on this below.
Built-in Commands
The two built-in commands are exit and cd, which the shell must
perform itself. After parsing the command,
you need to check if breaker[0] is equal to one of those
commands, and treat it as a special case.
In the case of exit,
a process can't very well let a another process terminate for it, so it
must do this itself. Simply make the
exit call.
If the command has an argument, it should be a number which you send as
the argument to the exit call. Otherwise, run exit(0).
You will need that parameter as an integer. The line breaker has a
method for this, or you can use
atoi.
Processes have a current directory, which is used as location for
simple file names. The cd command changes the current directory for
the shell process. Since the current directory is a process attribute,
it must be done by the shell itself. (If that's not obvious, think about
it a minute.)
Use the chdir call. If the
cd command has an argument, use it as the directory to change to.
If not, go to the home directory. You can find out the home directory
with the call getenv("HOME") (see
getenv). Recall that
either the getenv or chdir calls may fail; be sure to report
any failure.
Termination Status
When an external command finishes and the parent wait returns, you should
report how the process ended. If it exited with a zero exit code (normal
exit), don't print anything.
If it exited with a non-zero exit code,
report the code. If it crashed, report the cause of the crash.
“Crash” just means it was terminated with a Unix signal.
The
wait call
is used to wait for
the child process to finish
report the cause of termination to the parent.
The
execution example shows how to capture the termination
status, and tell if the process exited or crashed, using
WIFEXITED
or
WIFSIGNALED. Read the manual page for descriptions
WEXITSTATUS to find the exit code, and
WTERMSIG to discover the cause of a crash.
You will also want
strsignal
to convert the signal number into readable message.
Start File
If you like, you may download
a2start.zip, which is an archive
containing the line breaker class and its tester, along with a make file
and mostly empty file called
main.cpp. You can get started on
Sandbox (or another Unixish system) like this:
[tom@localhost tmp]$ wget http://sandbox.mc.edu/~bennet/cs422b/asst/a2start.zip
--2023-09-23 17:46:44-- http://sandbox.mc.edu/~bennet/cs422b/asst/a2start.zip
Resolving sandbox.mc.edu (sandbox.mc.edu)... 167.160.210.32
Connecting to sandbox.mc.edu (sandbox.mc.edu)|167.160.210.32|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3033 (3.0K) [application/zip]
Saving to: 'a2start.zip'
a2start.zip 100%[===================>] 2.96K --.-KB/s in 0s
2023-09-23 17:46:44 (41.3 MB/s) - 'a2start.zip' saved [3033/3033]
[tom@localhost tmp]$ mkdir a2start
[tom@localhost tmp]$ cd a2start
[tom@localhost a2start]$ unzip ../a2start.zip
Archive: ../a2start.zip
inflating: linebreaker.h
inflating: linebreaker.cpp
inflating: lbtest.cpp
inflating: main.cpp
inflating: Makefile
[tom@localhost a2start]$ make
g++ -c -o main.o main.cpp
g++ -c -o linebreaker.o linebreaker.cpp
c++ main.o linebreaker.o -o main
g++ -c -o lbtest.o lbtest.cpp
c++ lbtest.o linebreaker.o -o lbtest
[tom@localhost a2start]$ ./main
This program is not finished yet.
You can then write your program in
main.cpp and build it with make.
The starting code and my solution program compile and run fine on a
BSD kernel, so I would expect no trouble running on a Mac.
You may not have wget or zip commands
installed, but saving a2start.zip with a browser
and/or unpacking it with a GUI tool should
work just fine.
Submission
When your program works correctly and looks nice,
submit
it here. There's a place there to
send the linebreaker, but don't bother unless
you changed it.