While working on your shell, if you accidentally forkbomb, you will probably have to close your VM and restart it. (The bug is, you forgot to exit
after calling execvp
.)
In this project, you’ll be making a simple Unix shell. A shell is what you interact with when you log into your VM - it’s a command-line interface for running programs.
Unlike the previous programming projects/labs, you are being left to figure many things out for yourself. By now you should be familiar enough with “how things work” in C that you can be mostly self-sufficient, and figure out how to do things based on examples and documentation.
To that end, here are some useful places for documentation:
- cplusplus.com, good for looking up C standard library functions (e.g.
signal
,freopen
) - die.net, good for looking up POSIX functions (e.g.
execvp
,waitpid
)
Additionally, I have linked relevant code examples in the instructions below.
Grading Rubric
Note: Error handling is not explicitly mentioned in any of the following categories, but you should be checking for errors in everything. You’ll lose points if not.
- [10 points] Code style. This includes:
- Your code should compile with this command line:
gcc -Wall -Werror --std=c99 -g -o myshell myshell.c
- Splitting code into sensible functions. This is entirely up to you for this project.
- Properly deallocating any memory you allocate on the heap
- Naming things reasonably
- Your code should compile with this command line:
- [30 points] Builtins
- [10]
exit
exits the program - [10]
exit xx
exits the program with exit code xx - [10]
cd dir
changes the current directory todir
- [10]
- [40 points] Regular Programs
- [10] SIGINT (ctrl+C) is properly handled - does nothing in your shell, but can interrupt running programs
- [10] Can run a regular program, period
- [10] Can run a regular program with an arbitrary number of arguments
- [10] Displays a human-readable message about what signal killed a program (e.g. if you ctrl+C in
cat
)
- [20 points] Redirection
- [8] Input redirection
<
works and gives an error if input is redirected more than once - [8] Output redirection
>
works and gives an error if output is redirected more than once - [4] Input and output redirection can be used at the same time, in either order
- i.e.
cat < input > output
orcat > output < input
should do the same thing)
- i.e.
- [8] Input redirection
Isn’t a shell a special kind of program?
Nope! A shell is just a user-mode process that lets you interact with the operating system. The basic operation of a shell can be summed up as:
- Read a command from the user
- Parse that command
- If the command is valid, run it
- Go back to step 1
The shell you interact with when you log into your VM is called bash
. You can even run bash
inside itself:
[cs0449vm ~]: bash
[cs0449vm ~]: pstree student
sshd───bash───bash───pstree
systemd───(sd-pam)
[cs0449vm ~]: exit
[cs0449vm ~]: _
pstree
shows a bash
process nested inside another bash
process!
When you write your shell, you can test it like any other program you’ve written.
[cs0449vm ~]: ./myshell
myshell> ls
myshell myshell.c
myshell> exit
[cs0449vm ~]: _
1. Input tokenization
Use fgets()
with a generously-sized input buffer, like 300 characters.
Once you have the input, you can tokenize it (split it into “words”) with the strtok_r()
function. It behaves oddly, so be sure to read up on it.
Put #define _GNU_SOURCE
at the top of your program like in the following example.
Here is a sample program that demonstrates strtok_r
.. Feel free to use it as the basis for your command parsing, but remember… You cannot return the resulting token array from a function. So you have to allocate that array in the function that needs it.
For strtok_r()
’s “delim” parameter, you can give it this string:
" \t\n"
Get the string tokenization working first. Test it out well, and try edge cases - typing nothing, typing many things, typing several spaces in a row, using tab characters…
Commands
Many of the commands you’re used to running in the shell are actually builtins - commands that the shell understands and executes instead of having another program execute them. This makes the shell somewhat faster, because it doesn’t have to start a new process for each command.
Anything that isn’t a builtin should be interpreted as a command to run a program.
Following is a list of commands you need to support.
2. exit
and exit number
Functions needed: exit()
The simplest command is exit
, as it just… exits the shell.
NOTE: In all these examples, myshell>
indicates your shell’s prompt, and $
indicates bash’s prompt.
$ ./myshell
myshell> exit
$ _
You also need to support giving an argument to exit
. It should be a number, and it will be returned to bash. You can check it like so:
Exit codes can only go up to 255. If you type a bigger number, it will wrap around. That’s not a bug, so don’t worry!
myshell> exit 45
$ echo $?
45
$ _
The echo $?
command in bash will show the exit code from the last program.
If no argument is given to exit
, it should return 0:
myshell> exit
$ echo $?
0
$ _
Hint: there are a few functions in the C standard library you can use to parse integers from strings. You’ve used at least one before…
3. cd dirname
Functions needed: chdir()
You know how cd
works! You don’t have to do anything special for the stuff that comes after the cd
. chdir()
handles it all for you.
Really, chdir()
handles it all for you. You don’t have to parse the path, or look for ‘..’, or make sure paths are relative/absolute etc. chdir()
is like cd
in function form.
You do not need to support cd
without an argument. Just regular old cd
.
You do not need to support cd ~
. This is actually a bash feature, but it’s kind of complicated, so don’t worry about it.
You can see if it works properly using the pwd
program, once your shell can run regular programs.
myshell> cd test
myshell> pwd
/home/student/private/test
myshell> cd ..
myshell> pwd
/home/student/private
myshell> _
4. Regular programs
Functions needed: fork()
, execvp()
, exit()
, waitpid()
, signal()
If something doesn’t look like any built-in command, run it as a regular program. You should support commands with or without arguments.
There are two examples that will form the basis of this part, which you can kinda smash together:
- 17_exec.c shows how to execute a program in the child process.
- 17_waitpid.c shows how to properly use
waitpid()
in the parent process to diagnose how the child process exited.
Your shell should support ANY number of arguments to programs, not just zero or one.
For example, and these are just examples: ANY program should be able to be run like this:
myshell> ls
myshell.c myshell Makefile
myshell> pwd
/home/student/private
myshell> echo "hello"
"hello"
myshell> echo 1 2 3 4 5
1 2 3 4 5
myshell> touch one two three
myshell> ls -lh .
total 9K
-rw-r--r-- 1 abc123 UNKNOWN1 2.8K Dec 3 22:04 myshell.c
-rwxr-xr-x 1 abc123 UNKNOWN1 4.4K Dec 3 22:04 myshell
-rw-r--r-- 1 abc123 UNKNOWN1 319 Dec 3 18:51 Makefile
-rw-r--r-- 1 abc123 UNKNOWN1 0 Dec 3 22:05 one
-rw-r--r-- 1 abc123 UNKNOWN1 0 Dec 3 22:05 two
-rw-r--r-- 1 abc123 UNKNOWN1 0 Dec 3 22:05 three
myshell> _
The Parent Process
After using fork()
, the parent process should wait for its child to complete. Things to make sure to implement:
- Make sure to check the return value from
waitpid
to see if it failed. - If the child did not exit normally (
WIFEXITED
gives false):- if the child terminated due to a signal, print out which signal killed it.
- otherwise, just say it terminated abnormally.
If you get errors about “implicit declaration of function ‘strsignal’” then add #define _GNU_SOURCE
to the very top of your code, before any #include
lines.
The Child Process
After using fork()
, the child process is responsible for running the program. Things to make sure to implement:
- Set the SIGINT behavior to the default (as explained below in the Ctrl+C section).
- Use
execvp
to run the program. - Print an error if
execvp
failed.
AND THEN…. exit() after you print the error. DON’T FORGET TO EXIT HERE. This is how you forkbomb.
Notes on using execvp
:
- The way
execvp
detects how many arguments you’ve given it is by putting a NULL string pointer as the “last” argument. You must put the NULL in your arguments array yourself, after parsing the user input. (Thestrtok_r
example above does this.) execvp
only returns if it failed. So you don’t technically need to check its return value.
Catching Ctrl+C
Ctrl+C is a useful way to stop a running process. However by default, if you Ctrl+C while a child process is running, the parent will terminate too. So if you try to use it while running a program in your shell…
$ ./myshell
myshell> cat
typing stuff here...
typing stuff here...
cat just copies everything I type.
cat just copies everything I type.
<ctrl+C>
$ _
I tried to exit cat
by using Ctrl+C but it exited my shell too!
Making this work right is pretty easy.
- At the beginning of
main
, set it to ignore SIGINT. - In the child process (after
fork
but beforeexec
), set itsSIGINT
behavior to the default.
Once that’s done, you can use Ctrl+C with abandon:
Ctrl+C will make a ^C
print and kinda mess up the display. That’s okay.
$ ./myshell
myshell> cat
blah
blah
blahhhhh
blahhhhh
<ctrl+C>
myshell> exit
$ _
5. Input and Output redirection
Functions needed: freopen()
Any regular program should also support having its stdin, stdout, or both redirected with the <
and >
symbols.
The redirections can come in either order, like cat < input > output
or cat > output < input
. Do not hardcode your shell to assume one will come before the other.
Your shell should support using input and output redirection on any non-builtin command with any number of parameters.
This means you should look for the redirections by looking starting at the last tokens. Then you can replace each redirection token (<
and >
) with NULL to ensure the right arguments get passed to the program.
There should be at most one >
and one <
. If the user uses <
or >
more than once, give an error and don’t run anything. (Since you are in the child process when doing this, you can just exit(0)
when you encounter a redirection error.)
bash lets you write ls>out
without spaces, but you don’t have to support that. ls > out
is fine for your shell.
myshell> ls > output
myshell> cat output
myshell.c
myshell
Makefile
output
myshell> less < Makefile
<then less runs and shows the makefile>
myshell> cat < Makefile > copy
myshell> ls
myshell.c myshell Makefile output copy
myshell> less copy
<then less runs and shows that 'copy' is identical to the original makefile>
myshell> ls -lh . > output
myshell> cat output
total 31K
-rw-r--r-- 1 abc123 UNKNOWN1 2.8K Dec 3 23:18 myshell.c
-rwxr-xr-x 1 abc123 UNKNOWN1 4.4K Dec 3 23:18 myshell
-rw-r--r-- 1 abc123 UNKNOWN1 319 Dec 3 18:51 Makefile
-rw-r--r-- 1 abc123 UNKNOWN1 39 Dec 3 23:20 output
-rw-r--r-- 1 abc123 UNKNOWN1 319 Dec 3 23:21 copy
myshell> ls > output > output
Error: Too many redirections
myshell> _
Input and output redirection should detect and report the following errors:
- If the user tried to redirect stdin or stdout more than once
- e.g.
ls > out1 > out2
- or
cat < file1 < file2
- e.g.
- If the file to read or write could not be opened
Again, you can just exit the child process when encountering any of these errors.
Opening the redirection files
You should open the redirection files in the child process after using fork
, but before using execvp()
.
In order to redirect stdin and stdout, you have to open new files to take their place. freopen()
is the right choice for this.
- When opening a file for redirecting
stdin
, you want to open the file as read-only. - When opening a file for redirecting
stdout
, you want to open the file as write-only.
Submission
It’ll be on gradescooooope