Project 2: Command Line Shell (v 1.0)

The outermost layer of the operating system is called the shell. In Unix-based systems, the shell is generally a command line interface. Most Linux distributions ship with bash as the default (there are several others: csh, ksh, sh, tcsh, zsh). In this project, we’ll be implementing a shell of our own – see, I told you that you’d come to love command line interfaces in this class!

You will need to come up with a name for your shell first. In the following examples, my shell is named crash (Cool Really Awesome Shell) because of its tendency to crash.

The Basics

Upon startup, your shell will print its prompt and wait for user input. Your shell should be able to run commands with exec. If a command isn’t found, print an error message:

[0]-[1]─[/]$ ./hello
Hello world!
[0]-[2]─[/]$ ls /dir
(directory contents are shown by ls)
[0]-[3]─[/]$ echo hello there!
hello there!
[0]-[4]─[/]$ cd /this/does/not/exist
chdir: no such file or directory: /this/does/not/exist
[1]-[5]─[/]$ cd /this/does/exist
[0]-[6]─[/this/does/exist]$ 

Prompt

The shell prompt displays some helpful information. You can customize it to look however you want, but at a minimum, it must include:

In the example above, these are separated by dashes and brackets to make it a little easier to read. You’ll notice that when the cd built in command failed, the exit status was “1”.

Scripting Support

Your shell must support scripting mode, which also makes it easier to test. If argv[1] is a file, read commands from it instead of from stdin:

$ cat test.sh
ls /
echo hi
mkdir d
cd d
ls
/ls
/echo bye
exit

$ crash test.sh
(ls / contents appear)
hi
exec ls failed
.              1 35 32
..             1 1 1024
bye

You should check and make sure you can run a large script with your shell. Note that the shell prompt is not displayed when in scripting mode, and the script should not have to end with the exit command, even though it does here.

Executable Scripts

Beyond basic scripting support, you should also support executing scripts that begin with a “shebang” (#!), for instance #!/crash at the beginning of the script would execute the script with your shell. Executable script support is mainly provided via the OS program loader; you should modify the kernel to detect the presence of #! at the beginning of a shell script and then execute the interpreter with the script name as its first command line argument.

Here’s the same example as above, but with executable script support:

$ cat test.sh
#!/crash
ls /
echo hi
mkdir d
cd d
ls
/ls
/echo bye
exit

$ ./test.sh # notice how we didn't have to run 'crash' here
(ls / contents appear)
hi
exec ls failed
.              1 35 32
..             1 1 1024
bye

Built-In Commands

Most shells have built-in commands, including cd and exit. Your shell must support:

History

Here’s a demonstration of the history command:

[0]-[142]─[/]$ history
  42 ls
  43 cat long-text.txt
  44 echo "hi" # This prints out 'hi'

... (commands removed for brevity) ...

 140 ls /bin
 141 gcc -g crash.c

In this demo, the user has entered 142 commands. Only the last 100 are kept, so the list starts at command 43. If the user enters a blank command, it should not be shown in the history or increment the command counter.

Here’s the second form of the history command, history -t. This version shows the amount of time each command ran for:

[0]-[142]─[/]$ history -t
[42|68ms] ls
[43|982ms] cat long-text.txt
[44|13ms] echo "hi" # This prints out 'hi'

... (commands removed for brevity) ...

[140|32ms] ls /bin
[141|1ms] gcc -g crash.c

In both cases, the history command is not shown at the end of the output. For background processes, report the amount of time it took for the child process to start.

I/O Redirection

Your shell must support file input/output redirection and pipes:

# Create/overwrite 'my_file.txt' and redirect the output of echo there:
[0]-[142]─[/]$ echo "hello world!" > my_file.txt
[0]-[143]─[/]$ cat my_file.txt
hello world!

# Append text with '>>':
[0]-[144]─[/]$ echo "hello world!" >> my_file.txt
[0]-[145]─[/]$ cat my_file.txt
hello world!
hello world!

# Pipe:
[0]-[146]─[/]$ cat other_file.txt | sort
(sorted contents shown)

[0]-[147]─[/]$ seq 100000 | wc -l
10000

[0]-[148]─[/]$ cat /etc/passwd | sort > sorted_pwd.txt
(sorted contents written to 'sorted_pwd.txt')

# This is equivalent, but uses input redirection instead of cat:
[0]-[149]─[/]$ sort < /etc/passwd > sorted_pwd.txt

# This is equivalent as well. Order of < and > don't matter:
[0]-[150]─[/]$ sort > sorted_pwd.txt < /etc/passwd

# Here's input redirection by itself (not redirecting to a file):
[0]-[151]─[/]$ sort < sorted_pwd.txt
(sorted contents shown)

Use dup and pipe to provide redirection and pipe support. Your implementation should support arbitrary numbers of pipes, but you only need to support one file redirection per command line (i.e., a > or >> in the middle of a pipeline is not something you need to consider).

Background Jobs

If a command ends in &, then it should run in the background (do not pass & to the program as an argument). In other words, don’t wait for the command to finish before prompting for the next command.

Path Execution

You may have noticed that when you leave the / directory (cd somedir), suddenly programs begin failing to run unless you prefix them with a /, e.g., cat doesn’t work but /cat does. This is because we don’t have support for path execution – searching a path or set of paths for binaries. Instead, the default behavior is to simply run whatever is in the current directory. For this feature, you will create a new function in your shell called execvp(pathname, args) that will search for and run binaries based on priority. The priorities are:

  1. Absolute or relative paths are run first. E.g., ../ls runs the ls binary in the directory one level above, /cat runs cat from the root directory, etc.
  2. If an absolute/relative path wasn’t given, and the user simply typed a program’s name, then search the / (root) directory for it by default.
  3. If the previous step failed (the program doesn’t exist in /), then attempt to execute it from the current directory instead.

Make sure that your execvp function follows the hierarchy above.

Signature Feature

After you’re done implementing all the features above, you will need to choose to add one more feature to your shell. You can take inspiration from other shells, or come up with something completely new. It’s your choice – just make sure you run the idea by the instructor first.

Documentation

You must include documentation for your shell, located in docs/SHELLNAME.md. The documentation must describe your program, how it works, and any other relevant details. You’ll be happy you did this later if/when your revisit the codebase. Here is an example README.md file.

Kernel Changes

You will need to make the following modifications to your OS kernel as part of this project:

Grading and Submission

Check your changes into your OS repo as you work. We’ll do a live demo and code review to grade the project, so you should test your shell with a variety of commands to make sure it will work during the demo.

This project is worth 18 points.

To receive 60% credit, implement:

To receive 75% credit, implement:

To receive 85% credit, implement:

To receive 90% credit, implement:

To receive 95% credit, implement:

To receive full credit for this project:

Your grade will also include an additional 2 points from the code review / demo. Things that we will check: