#include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <string.h> // Function: void parse(char *line, char **argv) // Purpose : This function takes in a null terminated string pointed to by // <line>. It also takes in an array of pointers to char <argv>. // When the function returns, the string pointed to by the // pointer <line> has ALL of its whitespace characters (space, // tab, and newline) turned into null characters ('\0'). The // array of pointers to chars will be modified so that the zeroth // slot will point to the first non-null character in the string // pointed to by <line>, the oneth slot will point to the second // non-null character in the string pointed to by <line>, and so // on. In other words, each subsequent pointer in argv will point // to each subsequent "token" (characters separated by white space) // IN the block of memory stored at the pointer <line>. Since all // the white space is replaced by '\0', every one of these "tokens" // pointed to by subsequent entires of argv will be a valid string // The "last" entry in the argv array will be set to NULL. This // will mark the end of the tokens in the string. // void parse(char *line, char **argv) { // We will assume that the input string is NULL terminated. If it // is not, this code WILL break. The rewriting of whitespace characters // and the updating of pointers in argv are interleaved. Basically // we do a while loop that will go until we run out of characters in // the string (the outer while loop that goes until '\0'). Inside // that loop, we interleave between rewriting white space (space, tab, // and newline) with nulls ('\0') AND just skipping over non-whitespace. // Note that whenever we encounter a non-whitespace character, we record // that address in the array of address at argv and increment it. When // we run out of tokens in the string, we make the last entry in the array // at argv NULL. This marks the end of pointers to tokens. Easy, right? while (*line != '\0') // outer loop. keep going until the whole string is read { // keep moving forward the pointer into the input string until // we encounter a non-whitespace character. While we're at it, // turn all those whitespace characters we're seeing into null chars. while (*line == ' ' || *line == '\t' || *line == '\n' || *line == '\r') { *line = '\0'; line++; } // If I got this far, I MUST be looking at a non-whitespace character, // or, the beginning of a token. So, let's record the address of this // beginning of token to the address I'm pointing at now. (Put it in *argv) // then we'll increment argv so that the next time I store an address, it // will be in the next slot of the array of integers. *argv++ = line; /* save the argument position */ // Ok... now let's just keep incrementing the input line pointer until // I'm looking at whitespace again. This "eats" the token I just found // and sets me up to look for the next. while (*line != '\0' && *line != ' ' && *line != '\t' && *line != '\n' && *line !='\r') line++; /* skip the argument until ... */ } // Heh, I ran out of characters in the input string. I guess I'm out of tokens. // So, whatever slot of the array at argv I'm pointing at? Yeah, put a NULL // there so we can mark the end of entries in the table. *argv = NULL; /* mark the end of argument list */ } void execute(char **argv) { if (argv[1] == NULL) printf("I would fork() a process to execute %s if I knew how\n", *argv); else { printf("I would fork() a process to execute %s with the parameters\n", *argv++); while (*argv != NULL) printf(" %s\n", *argv++); printf("if I knew how\n"); } } int main(void) { char line[1024]; // This is the string buffer that will hold // the string typed in by the user. This // string will be parsed. The shell will do // what it needs to do based on the tokens it // finds. Note that a user may NOT type in // an input line of greater than 1024 characters // because that's the size of the array. char *largv[64]; // This is a pointer to an array of 64 pointers to // char, or, an array of pointers to strings. // after parsing, this array will hold pointers // to memory INSIDE of the string pointed to by // the pointer line. argv[0] will be the string // version of the first token inside of line... // argv[1] will be the second... and so on... // See the routine parse() for details. char shell_prompt[15]; // This string will hold the shell prompt string // set the default prompt strcpy(shell_prompt, "SillyShell"); // The shell by default goes forever... so... while forever ;) while (1) { printf("%s> ",shell_prompt); // display the shell prompt fgets(line, 1024, stdin); // use the safe fgets() function to read // the user's command line. Why wouldn't // we use gets() here? line[strlen(line)-1]='\0'; // This is a dirty hack. Figure it out maybe? if (*line != '\0') // If something was actually typed, then do something... { // First, get all the addresses of all of the tokens inside the input line parse(line, largv); // parse the line to break it into token references // Check the first token to see if there are any built in commands // we want to handle directly. Do this with an "if/then/else" ladder. // if we hit the end of the ladder, we assume the command line was requesting // an external program be run as a child process and do that.... if (strcmp(largv[0], "exit") == 0) exit(0); else if (strcmp(largv[0], "done") == 0) exit(0); else if (strcmp(largv[0], "newprompt") == 0) { if (largv[1] != NULL) strncpy(shell_prompt, largv[1], 15); else strncpy(shell_prompt, "SillyShell", 15); } else execute(largv); /* otherwise, execute the command */ } } }
Homework One: Operating Systems Internals and Design Fall Semester 2018
As we discussed in class and in the book, a shell is a type of user interface. Generally speaking,
a CLI (Command-LIne) shell is a text-based interface in which a user types text commands at a prompt, and the shell program reads and executes user commands. Shells can also include a
simple programming language (a scripting language) that a user could also use to automate
tasks that would otherwise require a great deal of typing from the command line. You can read
more about shells in general at:
https://en.wikipedia.org/wiki/Shell_(computing)
A simple CLI shell might have a processing loop that looks something like this:
while (shell_is_not_finished) { read a line of input; tokenize the line of input; if (first token is a built-in command)
{ do what the command says }
else { fork a clone of the shell;
Have the clone load the program named in the first token and pass it the tokens it has them too
} }
In English, what the shell does is this:
Go into an infinite loop of reading and interpreting command line input. For each line read,
FIRST “tokenize” the input. By tokenizing, we mean that we rewrite the input string so that all
white space in the string (space, tab, newline, and carriage return characters) are replaced by
null characters (ASCII code zero) and that we create an array of pointers that point to the first
NON ZERO (null) character of each cluster of non-null characters. This may sound complex, but
consider the following:
A user types “ls -l” at the command line. In this case, the user typed the “ls” command name, three spaces, and then the command line flag “-l” (long list option). In memory, the shell would maintain a buffer of characters typed that would look like this:
l S l-input_line \0
Where input_line is a variable of type pointer to character (char *) that points to the memory location where the FIRST character of the input data is stored. Each subsequent
memory location will hold subsequent characters in the input string. The first step is to change
all the “whitespace” characters to null characters, like this:
This could be done with a simple loop. After this step, you’d create another array, this time of
POINTERS to chars. Each pointer in THAT array would point to the first character in each of the
“separated words” in the input line. That would look something like this:
Now we have a new array. The first element of that array is pointed to by l_argv. Each subsequent slot of l_argv is a pointer to subsequent words in the input string. The array of pointers is itself terminated by a memory location containing NULL. Notice the format of our “l_argv” array is IDENTICAL to that of the argv array you’re already familiar with.
Once the above structure is created, then l_argv[0] will be the string that is the first TOKEN inside of the input. l_argv[1] will be the string that is the second TOKEN in in the input…. and so on.
In short, the process of parsing creates something that looks and acts just like the
char **argv parameter you could pass into main(), except of course it creates a tokenized version of user input instead of system input.
Once things are tokenized, your shell could look at the FIRST token to see if it is a “built-in” or a
“program name”. If it’s a built-in, then it should just call local code to do it. If it is not, it should
fork a process, load the text segment of that process with the program (passing to it any
parameters it should get from the command line) and then wait until the child is done. When
the child is done, then the shell can continue reading, parsing, and doing what it’s told line by
line.
You may want to examine the heavily commented code “sillyshell_template.c” at this point.
The template code will take in commands, parse them, and process a very small collection of
built-ins. It will not actually fork processes and run other programs in them. It will just
l S \0 \0 \0 l- \0input_line
S \0 \0 \0 l- \0l
NULL
l_argv
input_line
complain that it wish it could and return you to regular processing. Before moving on, make
sure you understand the template code.
For your assignment, you will need to complete the following programming tasks. EVERYTHING
you need is either in the book, explicitly mentioned in lecture or one of the in-class examples
you were asked to work, or is explicitly in the template code itself. You will need at least a basic
understanding of everything in those sources to do this assignment. Note that you get to a 60%
JUST by repeating things we did in class.
Task One: Add Simple Program Calling (50 points) For task one, modify the program so that that when the token pointed to by largv[0] is NOT a built-in command, your sillyshell will do the following: a) fork a process b) have the child
process load the program in the file specified by largv[0] and be passed the appropriate command line arguments. c) have the parent wait on the completion of the child, then return
to normal processing of input lines. Note, ALL of modifications you need to make could be
done INSIDE the sillyshell routine called execute(). Also note that this task is nearly identical to a task we did during an in-class activity.
Task Two: Add a Built-In Command that Prints out All Environment Variables (10 points) For task two, add a new built-in command called “printenv” that prints to the screen ALL of the current shell’s environment variables. This will require you to include a slight variation on
code you would have developed during an in-class exercise.
Task Three: Properly Handle Control Codes (20 points) Generally speaking, shells should NOT react to signals in the way that other processes might.
For example, typing control-c USUALLY interrupts a running process. A shell should not shut
down just because someone types control-c. Also, a shell USUALLY “shuts down” when
someone types control-d. The template I gave you goes into an endless loop if you try that
(yes, this is an intentional bug). For this task, you should make silly shell PROPERLY handle both
control-c and control-d. When you are running silly shell, typing control-c should have no effect
when you are at a prompt. Typing control-d should make sillyshell quit. Adding each capability
is worth 10 points each. Note, you’ll want to handle the control-c problem with material you
can find here: https://www.usna.edu/Users/cs/aviv/classes/ic221/s16/lec/19/lec.html
You will want to handle the control-d problem by investigating the fgets() routine and finding out what it returns if anyone types control-d.
Task Four: Putting a Command in the Background (20 points) In many standard shells, typing the & character as the last token for something that is not a
built-in will put the child process “in the background”. This means that the child process
created will NOT block the shell. The child and the shell will run at the same time and the shell
will continue accepting and running command lines even before the child terminates. Actual
shells also have job control commands that enable you to manipulate background jobs using
additional built-in commands. You can see some details at
https://www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html
Note that for this assignment, I am not requiring job control commands be added to your
sillyshell. For purposes of this task, you can put jobs “in the background” in any number and
you are not required to have any mechanism by which silly shell can interact with them after
they are created. Note, though, that REAL shells would always have such capability.
Task Five: Adding Job Control Capability Do NOT attempt this until you have verified that EVERYTHING ELSE is perfect. In this task, you
should describe a job control mechanism of your design that mimics at least some of the
abilities of the BASH shell’s job control capability. Tell me about your JCL built-ins and what
they do. Provide me with screen dumps showing us their use with real processes. Points will
be assigned according to the completeness of the JCL and will be applied against the NEXT
assignment (I.E. if you don’t get all the points on assignment #2, you can “spend” your bonus
points to make up the difference).
You should turn in a si e i i e that contains your C language source code for your assignment. With TASK FIVE, there should also be a text file that explains what job control capability you attempted. Your code should be HIGHLY commented. Please make your
comments descriptive of your thinking processes so that we are in the best position to give
partial credit if for some reason there are bugs in your code. You should also include in the zip file screen dumps and/or descriptions of how you tested each capability. The more evidence of
functionality you can provide, the better. The instructor and TA will also be compiling your
code and running our own tests. I will link a brief video lecture that explains how I will test your
code. If you can pass all the tests, you’ll get full credit.