This program was made by a team of two. It emulates the functionality of a shell,
allowing users to run programs and commands, pipeline processes, create background
jobs, and even redirect input and output.
The implementation of this program goes through 5 distinct steps for each input:
- The user input is read, parsed and broken into smaller arguments or commands.
- (If needed) Multiple commands are linked together in the case of pipelining.
- (If needed) The output of the final command is redirected to a file in the
case of redirection. - Each command is executed as a child of the parent process (possibly as a
background process) - All dynamically stored memory is freed.
The implementation of a basic shell starts with a simple loop which prints
a prompt and takes in input. This input is stored in a variable, cmd. It
then checks for a few edge cases such as no input being given before it
parses the input.
Parsing the input is a two step process with functions createCommandList()
and createCommandArg(). The former uses the latter to build its structure.
The parsing process for each user input follows three distinct steps:
- Separating the commands in the case of a pipline.
Since the input could contain multiple commands piped together we store
the commands in a struct that contains a struct containting the info of
each command (commandLineArg()) and a linked list to the next command.
First we take the user's input and use strtok_r() to tokenize the input
into multiple commands with "|" as the delimiter. - Separating destination output from the command in the case of redirection.
For each of the outputs/commands of strtok_r (from step 1) we use a separate
function to check if the commands contains a redirection. If it does we
use strtok() to tokenize the executable command (aka the part of the input
before ">") and then we store the destination and the type of redirection
(> or >>) in a commandLineArg() struct. - Separating the command into arguments and populating a struct.
In this step we take the output of step 2 (which is just a command) and then
we malloc an array of char pointers to store the arguments. Then we use strtok()
to tokenize the command with the " " delimiter and use malloc to allocate memory
for each of the words in the command. These memory addresses are then added to
the array of char pointers. This array of char pointers is then added to the
commandLineArg() struct.
Since the shell's behavior will substancially change if its executing a
background process, it checks for this right before it parses the input. The
implementation simply checks the last token of the input for an ampersand
symbol. If one is detected it replaces the symbol with a "" and returns 1,
otherwise it returns 0. The removal of the symbol is done since it's only
there to notify the shell it wants to be run in the background.
Now that the command has been parsed, it'll run through the built in
commands (exit, cd, etc) since the work involved with those is specific.
If none of those commands match, it then fork() to create a child process
for the pipeline.
Since we want our command(s) to execute before looping back to the shell
prompt, we encompass our pipeline inside a child process. The parent
process simply waits for the pipeline process to finish its execution
before freeing the memory of the commands and looping back to the prompt.
If the job being executing wishes to be run in the background, then the
parent doesn't wait for the pipeline to finish before looping back to
the shell prompt.
Inside the child process lies the pipeline. The pipeline takes in the
head of a linked list as a pointer and iterates through until the
currentPtr->next is equal to NULL.
It goes through four distinct steps at each iteration
- The process creates a pipe and forks.
This is done so that we can establish a connection between the child and
parent process. - The child process links the write port to stdout and executes the command
Doing this links the output of the current command to the input of the next
one. The process of executing the command is detailed next. - The parent process waits for the child and records its return status
The parent process remains the same from the first iteration to the last.
Creating child processes for each command allows the shell to record each
return value. - Lastly, the read port is linked to stdin and the pipe is closed
The parent process takes in the input of the stdin to finish connecting
the pipeline process.
This loop iterates until we reach currentPtr->next is equal to NULL. Once
reached, we know that we are on the last command so we apply the same
process of forking the process to execute, while the parent records,
except this time we print the return values for the entire chain of
commands before finally killing the process.
When executing a command, a function recieves a struct containing
the command, arguments, output directory, and output type
(> or >>) of the command to be executed. First, the
function uses an if statement to check if the command is "pwd" or
"cd" as they are built in commands. Then we check if the command will be
redirected (by seeing if the destination is not NULL) and if so
to redirect the output of the command a separate function (details of which
are located below). Once that is complete the function uses execvp to execute
the command and all of its arguments.
When a command wants to redirect the output to a file a function is called.
This function recieves a redirection type (1 or 2 pertaining to > or >>
respectively) and a destination. If the type is 1 then the file is opened or
created in truncate(O_TRUNC) mode and if the type is 2 then the file is opened
or created in append(O_APPEND) mode. If the file cannot be opened an error is
returned. Finally the function uses dup2 to redirect the stdout to the file, the
file is closed, and the function ends.
After a command is executed the memory used to store all of its arguments and
pipline commands must be freed. The parent function that loops through main does
this after each child/command is finished by running the freeCommands function.
This function takes in the head pointer of the struct command and then uses a
while loop to loop until the next pointer in command is null. Within the loop is
uses free() to free up the memory for each of the arguments in the struct
commandLineArg variable, the directory redirection variable of the struct
commandLineArg (if not equal to NULL), the struct commandLineArg variable
itself, as well as the struct command variable.