C语言代写: CS241 Shell

Well, we’ll keep it short, you got fired. Your boss brought you in for a code review and was more than disappointed. Apparently, he wanted a text editor like this one: we didn’t get the memo. Now it’s time to prove your worth. Your boss wanted something fully functional and we’ve got a great suggestion (NOTE: this is not a suggestion but a required assignment). You’re going to drop a :fire: :fire: shell on him to get rehired. The basic function of a shell is to accept commands as inputs and execute the corresponding programs in response. You will be provided the LogVector, and format libraries for your use. Hopefully, this will make things right and you can resume your work at insert hot tech company here. Feel free to refer to the unix shell as a rough reference. If you want to build a kernel that actually uses your shell we cannot offer you some extra credit, but we really can’t stop you from doing that.

format.h

Since this MP requires your programs to print a variety of things like error messages, we have provided you with our own highly customized formatting library. You should not be printing out to stdout and stderr at all; instead, all output and errors should be printed using the functions provided in format.c and format.h. In format.h you can find documentation of what each function does and you should use them whenever approriate. This is our way of ensuring that you do not lose points for formatting issues, but it also means that you are responsible for handling any and all errors mentioned in format.c and format.h.

Overview

The shell is responsible for providing a command line for users to execute programs or scripts. You should be very familiar with bash by now, which will be the basis for your own shell. Beyond the basics, your shell will be providing a few extra features, including executing programs in the background, keeping a command history, and running scripts.

Starting your shell

The shell should run in a loop like this executing multiple commands:

  • Print a command prompt
  • Read the command from standard input
  • Print the PID of the process executing the command (with the exception of built-in commands), and run the command

The shell must support the two following arguments:

History

-h takes the filename of the history file. The shell should load in the history file as its history. Upon exit, the exact same history file should be updated, even if cd is used in the shell.

./shell -h <filename>

Even if the the -h flag is not specified, the shell will still keep a history of commands run. Just think of it like private browsing mode for your terminal.

File

-f takes the name of the file to be executed by the shell. The shell will both print and run the commands in the file in sequential order until the end of the file. See the following example file and execution:

commands.txt

cd cs241
echo Hey!
./shell -f commands.txt
(pid=1234)/home/user$ cd cs241
(pid=1234)/home/user/cs241$ echo Hey!
Command executed by pid=1235
Hey!

The user may supply these arguments together, separately, or not at all, and in any order. Each of these cases must be supported, and the appropriate errors should be printed out if the user supplies invalid arguments.

The getopt function may come in handy. :smile:

Specifics

Prompting

When prompting for a command, the shell will print a prompt in the following format:

(pid=<pid>)<path>$

<pid> is the current process ID, and <path> is a path to the current working directory. Note the lack of a newline at the end of this prompt.

Reading in the Command

The shell will read in a command from stdin (or a file if -f was specified).

Running the Command

The shell should run the command that was read in previously.

If the command is run by a new process, the PID of the process should be printed like this:

Command executed by pid=<pid>

This should be printed before any of the output of the command is printed.

History

Your shell should store the command that was just executed. Every command should be stored unless otherwise noted. We provide to you a log data structure to simplify handling this history, which can be found in log.h.

Catch Ctrl+C

Usually when we do Ctrl+C, the current running program will exit. However, we want the shell to ignore the Ctrl+C signal (SIGINT). The shell should not exit upon receiving SIGINT.

Exiting

The shell will exit once it receives a EOF. From the interactive command line, this is sent by typing Ctrl+D on an empty line, and from a script file (as used with the -f flag) this is sent once the end of the file is reached. The history file (if specified) should be saved after EOF is received.

Commands

Shell supports two types of commands: built-in and external (i.e. non-built-in.) While built-in commands are executed without creating a new process, an external command must create a new process to execute the program for that particular command.

Command arguments will be space separated without trailing whitespace. Your shell does not need to support quotes (for example, echo "hello there"). Keep it simple.

Built-in Commands

cd <path>

Changes the current working directory of the shell to <path>. Paths not starting with / should be followed relative to the current directory. If the directory does not exist, then print the appropriate error.

(pid=1234)/home/user$ cd code
(pid=1234)/home/user/code$ cd imaginary_directory
imaginary_directory: No such file or directory
(pid=1234)/home/user/code$

!history

Prints out each command in the history, in order. Do not store this command in the history.

(pid=1234)/home/user$ !history
0    ls -l
1    pwd
2    ps
(pid=1234)/home/user$

:warning: This command is not stored in history.

#<n>

Prints and executes the nth command in history (chronological order), where n is a non-negative integer. Other values of n will not be tested. Note that the command run should be stored in the history. If n is not a valid index then print the appropriate error and do not store anything in the history. Note that this command is never stored in the history, only the command that is being referenced in the history.

The following example assumes a fresh history:

(pid=1234)/home/user$ echo Echo This!
Command executed by pid=1235
Echo This!
(pid=1234)/home/user$ echo Another echo
Command executed by pid=1236
Another echo
(pid=1234)/home/user$ !history
0    echo Echo This!
1    echo Another echo
(pid=1234)/home/user$ #1
echo Another echo
Command executed by pid=1237
Another echo
(pid=1234)/home/user$ #9001
Invalid Index
(pid=1234)/home/user$ !history
0    echo Echo This!
1    echo Another echo
2    echo Another echo
(pid=1234)/home/user$

:warning: Print out the command before executing if there is a match.

:warning: This command is not stored in history, only the command being executed (if any).

!<prefix>

Prints and executes the last command that has the specified prefix. Note that the command run should be stored in the history. If no match is found print the appropriate error and do not store anything in the history. Note that this command is never stored in the history, only the command that is being referenced in the history. The following example assumes a fresh history:

(pid=1234)/home/user$ echo Echo This!
Command executed by pid=1235
Echo This!
(pid=1234)/home/user$ echo Another echo
Command executed by pid=1236
Another echo
(pid=1234)/home/user$ !e
echo Another echo
Command executed by pid=1237
Another echo
(pid=1234)/home/user$ !echo E
echo Echo This!
Command executed by pid=1238
Echo This!
(pid=1234)/home/user$ !d
No Match
(pid=1234)/home/user$ !
echo Echo This!
Command executed by pid=1239
Echo This!
(pid=1234)/home/user$ !history
0    echo Echo This!
1    echo Another echo
2    echo Another echo
3    echo Echo This!
4    echo Echo This!
(pid=1234)/home/user$

:warning: Print out the command before executing if there is a match.

:warning: This command is not stored in history, only the command being executed (if any).

Invalid built-in commands

Note that invalid built-in commands should be stored in the history.

(pid=1234)/home/user$ cd /imaginary_directory
/imaginary_directory: No such file or directory
(pid=1234)/home/user$ !history
0    cd /imaginary_directory
(pid=1234)/home/user$

External Commands

For commands that are not built-in, the 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. You must use fork()exec(), and wait().

The fork()exec()wait() paradigm is as follows: 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. You are responsible of cleaning up all the child processes upon termination of your program. 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. If any of fork()exec(), or wait() fail, the appropriate error should be printed. For example:

(pid=1234)/home/user$ invalid_command --flag-that-is-ignored also_ignored also_ignored
Command executed by pid=1235
invalid_command: not found
(pid=1234)/home/user$

:warning: All external commands should be stored in the history, even ones that are invalid.

Some external commands that you may try to see whether your shell works as it should are:

/bin/ls
pwd
ps
echo hello

It is good practice to flush the standard output stream before the fork to be able to correctly display the output.

:bangbang: Please read the disclaimer at the top of the page! We don’t want to have to give any failing grades. :bangbang:

&

A command suffixed with & should be run in the background. The shell should be ready to take the next command before the given command has finished running. There may or may not be a single space between the rest of the command and &. For example, pwd& and pwd & are both valid. Additionally, since spawning a background process introduces a race condition, it is okay if the prompt gets misaligned as in the following example:

(pid=1873)/home/user$ pwd &
(pid=1873)/home/user$ Command executed by pid=1874
/home/user
When I type, it shows up on this line

While the shell should be usable after calling the command, after the process finishes the parent is still responsible for waiting on the child (hint: catch a signal). Avoid creating zombies!

Grading, Submission, and Other Details

Please fully read details on Academic Honesty. These are shared between all MPs in CS 241.

We will be using Subversion as our hand-in system this semester. Our grading system will checkout your most recent (pre-deadline) commit for grading. Therefore, to hand in your code, all you have to do is commit it to your Subversion repository.

To check out the provided code for shell from the class repository, go to your cs241 directory (the one you checked out for “know your tools”) and run:

svn up

If you run ls you will now see a shell folder, where you can find this assignment! To commit your changes (send them to us) type:

svn ci -m "shell submission"

Your repository directory can be viewed from a web browser from the following URL: https://subversion.ews.illinois.edu/svn/fa16-cs241/NETID/shell where NETID is your University NetID. It is important to check that the files you expect to be graded are present and up to date in your svn copy.

Compile and Run

Because we have provided Log and Vector as precompiled archive files, please make sure to work on this assignment on your student VM. We can’t say what will happen on any other machine when you try to compile the assignment.

To compile the release version of the code run:

make

This will compile your code with some optimizations enabled. If you use a debugger on the ‘release’ build, it will not be able to show you the original source code, or line numbers, most of the time. Optimizations sometimes expose some bugs in your code that would not show up when no optimizations are enabled, but since optimizations tend to reorder your code while compiling, an optimized version of your code is not optimal for debugging.

To compile your code in debug mode, run make debug instead of make

If you compile in release mode, you will an executable called shell. If you compile in debug mode, you will get an executable call shell-debug.