In this MP, you will implement a simple Unix shell interpreter (e.g., bash, csh etc.) called Shell. The basic function of a shell is to accept commands as inputs and execute the corresponding programs in response.
The purpose of this MP is to help you learn the basics of system calls for creating and managing processes as you implement your Shell program. You will write the code for your Shell program in shell.c inside your mp3 directory. The two other files you will write inside your mp3 directory: log.c and log.h will provide the implementation of a generic data structure log_t that you will use to implement the history feature of your Shell.
If you are not sure about the details of how shell should behave in some circumstances, Check this Piazza Post
You will implement the data structure: log_t. You can find details on this data structure in the source code or the generated documentation.
Print the PID of the process executing the command
NOTE : For following Built-In commands "cd", "exit", "!#" and "!query if and only if it matches cd", you don't need to print the line.
Your shell will support 4 arguments which you can pass when you run the shell: -h, -f, =
-h
./shell -h
This should print: "Shell by ["netid_of_the_student"] and then run the shell
-f
./shell -f ["filename"]
This should execute the commands in the script with the file name ["filename"]. (Follow the instructions starting from Task 2, but read from a file instead of stdin)
[NOTE 1] It's safe to use "system()" here since the autograder only cares about the output of the commands in the script file.
[NOTE 2] "exit" command will not appear in the test script since there will be other test cases from stdin after the "./shell -f XXX" command. (The sample binary uses "system" function here so the shell won't exit when an "exit" command comes.)
=
./shell =
This should print the list of environment variables in your machine. Your output may look
something like this:
-t
./shell -t
Whenever you run a NON-BUILT-IN COMMAND (Task 4), print the time it took for the command to execute.
For example, if you enter
Your shell will be run with no more than 1 argument at a time
Your shell prompt MUST use the following format:
(pid=x)/path/to/cwd$You can use printf("(pid=%d)%s$ ", pid, cwd) to print out this prompt. Here pid is the current process ID and cwd is the current working directory. Use getpid() to determine the process ID of the current process and getcwd() to lookup the current working directory.
Read a line from standard input. This line will be your command. The function getline() can be used to accomplish this easily. Beware that getline() may allocate memory that the user must free.
Command executed by pid=xYou can use printf(" Command executed by pid=%d\n", pid) to print out this process information. Here pid is the PID of the process that executes the command.
Shell supports two types of commands: built-in and non built-in. While built-in commands are executed without creating a new process, a non built-in command MUST create a new process to execute the program for that particular command.
For the purpose of simplicity, all commands we will test will contain no extra whitespace anywhere in the command (or extra junk that doesn't make sense like the " abc" in "!# abc"). This includes before the command, after the command, or between different arguments. We also will not test a blank line (though your program should not crash on a blank line).
Your Shell will support four built-in commands: cd, exit and two commands (!# and !) based on a history feature. You should use the log from Part 1 to implement the history functionality.
cd xxx
Changes the Shell's current working directory to 'xxx'. [Hint: See function chdir().]
If for some reason (for example, xxx is not a valid directory) the command "cd" does not end successfully, Shell should retain its current working directory and should print (without the quotes): "xxx: No such file or directory\n" to indicate the error.
exit
Terminates the Shell.
!#
Prints a list of all the commands saved in the history separated by newlines. Oldest commands should print first. Do not store !# in the history (this is the only command that will never store data in the history). For example:
ls -l
pwd
ps
!query
Re-executes the last command whose prefix matches query. Query can be any combination of characters. Print the match with printf("%s matches %s\n", query, match) before executing the command. If no match is found print "No Match". Re-executed commands should be stored in the history. Nothing will be stored in the history if no match is made. For example:
$ ls -l
...
$ ls
...
$ !l (re-executes "ls" and stores "ls" in history, not "!1")
...
!ls - (re-executes "ls -l" and stores "ls -l" in history)
...
!d (no match, does nothing and stores nothing in history)
No Match
If the command is not a Shell built-in (i.e., any command other than
cd, exit, !# and !query), Shell should consider the command name to be
the name of a file that contains executable binary code. Such a code
must be executed in a process different from the one executing the
shell. There are two ways to execute these commands in your shell: you
can either use system(), or you can use fork(), exec(), wait().
While using system() does not have the risk of creating a fork bomb, your shell would fail to print out the correct PID (in Task 3) when executing a non built-in command. Hence, with system(), you can receive only upto 75% of the total program execution score. In order to receive full credit, you must use fork(), exec(), wait().
When implementing the second option, your shell must fork() a child process. The child process must execute the command with exec(), while the parent must wait() for the child to terminate before printing the next prompt. It is important to note that, upon a successful execution of the
command, exec() never returns to the child process. exec() only returns to the child process when the command fails
to execute successfully.
In that case, shell should print (without the quotes): "xxx: not found\n", where xxx is the command.
Failure to terminate the child process after an invalid command may
result in a fork bomb. You can find information about cleaning a system
after a fork bomb here.
If your code fork bombs during grading, the autograder
will not be able to continue. This often results in the grader
unable to record meaningful output from your program and will be a 0 if
the autograder does not record output!
Some non built-in commands that you may try to see whether your Shell works as it should are: ls, /bin/ls,pwd, ps, echo hello.
ls -l
pwd
ps
!query
Re-executes the last command whose prefix matches query. Query can be any combination of characters. Print the match with printf("%s matches %s\n", query, match) before executing the command. If no match is found print "No Match". Re-executed commands should be stored in the history. Nothing will be stored in the history if no match is made. For example:
$ ls -l
...
$ ls
...
$ !l (re-executes "ls" and stores "ls" in history, not "!1")
...
!ls - (re-executes "ls -l" and stores "ls -l" in history)
...
!d (no match, does nothing and stores nothing in history)
No Match
A command suffixed with & should be run in the background. (i.e. The shell should be ready to take the next command before the given command has finished running)
Usually when we do *Ctrl+C*, the current running program will exit. However, we want the shell to ignore the *Ctrl+C signal*. (i.e. shell will not exit when a *Ctrl+C* signal comes).
To compile and run your shell, run the following commands from a Terminal on a Linux machine:
$ make
$ ./shell [args]
Type "exit" to exit from Shell (or if this has not yet been implemented, Ctrl+C will work).
Please fully read Grading Policy for more details on grading, submission, and other topics that are shared between all MPs in CS 241.