Skip to content
/ CShell Public

This program emulates the functionality of a shell, allowing users to run programs and commands, pipeline processes, create background jobs, and even redirect input and output.

License

Notifications You must be signed in to change notification settings

bhillig/CShell

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

CShell

About

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.

Implementation

The implementation of this program goes through 5 distinct steps for each input:

  1. The user input is read, parsed and broken into smaller arguments or commands.
  2. (If needed) Multiple commands are linked together in the case of pipelining.
  3. (If needed) The output of the final command is redirected to a file in the
    case of redirection.
  4. Each command is executed as a child of the parent process (possibly as a
    background process)
  5. All dynamically stored memory is freed.

Printing the prompt

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.

Parsing the user's input

The parsing process for each user input follows three distinct steps:

  1. 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.
  2. 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.
  3. 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.

Checking for background jobs

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.

Built in commands

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.

The Pipeline process

Forking the main process

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.

The Pipeline Function

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

  1. The process creates a pipe and forks.
    This is done so that we can establish a connection between the child and
    parent process.
  2. 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.
  3. 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.
  4. 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.

Executing the command

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.

Changing the output

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.

Freeing the memory

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.

About

This program emulates the functionality of a shell, allowing users to run programs and commands, pipeline processes, create background jobs, and even redirect input and output.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published