This project is inspired by the assignment taken from the great textbook Operating Systems: Three Easy Pieces (OSTEP); written by Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau from UW-Madison.
MIT License
Copyright (c) [2023] [Nguyen Ho]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A command line interpreter (CLI) or, as it is more commonly known, a shell is custom-built using C. The shell should operate in this basic way: when user type in a command (in response to its prompt), the shell creates a child process that executes the command the user entered and then prompts for more user input when it has finished.
wishThe basic shell, called wish (short for Wisconsin Shell, naturally), is
basically an interactive loop: it repeatedly prints a prompt wish> (note
the space after the greater-than sign), parses the input, executes the command
specified on that line of input, and waits for the command to finish. This is
repeated until the user types exit. The name of the final executable
should be wish.
The shell can be invoked with either no arguments or a single argument; anything else is an error. Here is the no-argument way:
prompt> ./wish
wish>
At this point, wish is running, and ready to accept commands. Type away!
The mode above is called interactive mode, and allows the user to type
commands directly. The shell also supports a batch mode, which instead reads
input from a batch file and executes commands from therein. Here is how to
run the shell with a batch file named batch.txt:
prompt> ./wish batch.txt
One difference between batch and interactive modes: in interactive mode, a
prompt is printed (wish> ). In batch mode, no prompt should be printed.
The shell is very simple (conceptually): it runs in a while loop, repeatedly
asking for input to tell it what command to execute. It then executes that
command. The loop continues indefinitely, until the user types the built-in
command exit, at which point it exits. That's it!
For reading lines of input, use getline(). This allows us to obtain arbitrarily
long input lines with ease. Generally, the shell will be run in interactive mode,
where the user types a command (one at a time) and the shell acts on it. However,
the shell will also support batch mode, in which the shell is given an input
file of commands; in this case, the shellshould not read user input (from stdin)
but rather from this file to get thecommands to execute.
In either mode, if it hitd the end-of-file marker (EOF), call exit(0).
To parse the input line into constituent pieces, use strsep().
To execute commands, use fork(), exec(), and wait()/waitpid().
If necessary, read the relevant book chapter
for a brief overview.
A path variable to describe the set of directories to search for executables; the set of directories that comprise the path are sometimes called the search path of the shell. The path variable contains the list of all directories to search, in order, when the user types a command.
Important: Note that the shell itself does not implement ls or other
commands (except built-ins). All it does is find those executables in one of
the directories specified by path and create a new process to run them.
To check if a particular file exists in a directory and is executable,
consider the access() system call. For example, when the user types ls,
and path is set to include both /bin and /usr/bin, try access("/bin/ls", X_OK). If that fails, try "/usr/bin/ls". If that fails too, it is an error.
The initial shell path contains one directory: `/bin'
Whenever the shell accepts a command, it should check whether the command is
a built-in command or not. If it is, it should not be executed like other
programs. Instead, the shell will invoke the implementation of the built-in
command. For example, to implement the exit built-in command, simply
call exit(0); in wish source code, which then will exit the shell.
In this project, we implement exit, cd, and path as built-in
commands.
exit: When the user types exit, the shell should simply call the exit
system call with 0 as a parameter. It is an error to pass any arguments to
exit.
cd: cd always take one argument (0 or >1 args should be signaled as an
error). To change directories, use the chdir() system call with the argument
supplied by the user; if chdir fails, that is also an error.
path: The path command takes 0 or more arguments, with each argument
separated by whitespace from the others. A typical usage would be like this:
wish> path /bin /usr/bin, which would add /bin and /usr/bin to the
search path of the shell. If the user sets path to be empty, then the shell
should not be able to run any programs (except built-in commands). The
path command always overwrites the old path with the newly specified
path.
Many times, a shell user prefers to send the output of a program to a file
rather than to the screen. Usually, a shell provides this nice feature with
the > character. Formally this is named as redirection of standard
output. This custom will implement this feature as well.
For example, if a user types ls -la /tmp > output, nothing should be printed
on the screen. Instead, the standard output of the ls program should be
rerouted to the file output. In addition, the standard error output of
the program should be rerouted to the file output (the twist is that this
is a little different than standard redirection).
If the output file exists before running the program, it will overwrite
the existing file (after truncating it).
The exact format of redirection is a command (and possibly some arguments) followed by the redirection symbol followed by a filename. Multiple redirection operators or multiple files to the right of the redirection sign are errors.
Note: no redirection for built-in commands
(e.g., no such thing as path /bin > file).
The shell will also allow the user to launch parallel commands. This is accomplished with the ampersand operator as follows:
wish> cmd1 & cmd2 args1 args2 & cmd3 args1
In this case, instead of running cmd1 and then waiting for it to finish,
the shell should run cmd1, cmd2, and cmd3 (each with whatever arguments
the user has passed to it) in parallel, before waiting for any of them to
complete.
Then, after starting all such processes, it must make sure to use wait()
(or waitpid) to wait for them to complete. After all processes are done,
return control to the user as usual (or, if in batch mode, move on to the next
line).
Print this one and only error message whenever an error is encountered:
char error_message[30] = "An error has occurred\n";
write(STDERR_FILENO, error_message, strlen(error_message));
The error message should be printed to stderr (standard error), as shown above.
After any most errors, the shell simply continue processing after
printing the one and only error message. However, if the shell is invoked with
more than one file, or if the shell is passed a bad batch file, it should exit
by calling exit(1).
There is a difference between errors that shell catches and those that
the program catches. The shell should catch all the syntax errors specified
in this project page. If the syntax of the command looks perfect, the shell
simply run the specified program. If there are any program-related errors (e.g.,
invalid arguments to ls when running it, for example), the shell does not
have to worry about that (rather, the program will print its own error
messages and exit).