Shell Simulation in Linux

Today we will be implementing the functionality of a shell in Linux.

Lets start by explaining what a shell is. Before the days of the graphical user interface (buttons, desktop icons, windows) the only way a user could interact with an operating system was by directly feeding it commands. These commands were entered into a black and white terminal that a human could communicate with. Here are some examples of commands you would use to interact with the Linux operating system.

  • ls – List the directories and files within our current directory.
  • touch – Create a new file.
  • cd ./my/directory/ – From the current directory, traverse inside of the my folder and then the directory folder.

These are only three of hundreds of Linux commands you would need to use if you were on a Linux system with no Graphical User Interface. This is also what we will be building today, using C and Linux system calls (functions we can use to interact with Linux). This simulated shell will even be able to pipe the data returned from one command, into another command.

Let us begin by entering the C libraries we will need for function calls throughout our program. The comments will explain their purpose.

#include <sys/types.h> //Used for the pid_t data type
#include <sys/wait.h>  //Used for the wait() function
#include <stdio.h>     //Used for entering input and retrieving output
#include <stdlib.h>    //Contains functions for manipulating strings, memory, ect
#include <unistd.h>    //Contains our needed Linux system calls
 
#define TRUE 1         //Define true and false (we could have used <stdbool.h>)
#define FALSE 0

At first glance, you may be confused as to what pid_t, wait(), and Linux system calls are.
Dont worry! All the details will be explained as we go along.

Next we will define our main function, where execution of our program begins. Since we are not passing any command line arguments into main, we can just leave the argument void.

int main(void)
{
    char buf[1024]      = {'\0'};  //Hold the entire command entered by a user
    char *command[10]   = {'\0'};  //Each string holds a command on either side of the pipe command
    char *arguments[9]  = {'\0'};  //Each string holds an argument for an individual command
    char *token_ptr;               //An iterator for the strtok function
    pid_t pid1, pid2;              //Used to hold the process ID of the two children
 
    /*Various indexes, flags, and variables used throughout the program*/
    int status = 0, command_index = 0, argument_index = 0;
    bool quit = FALSE, display_shell = TRUE;
    /* Index 0 = Read end of Pipe
       Index 1 = Write end of pipe*/
    int	pipe_fd[2];
 
    /*Remove any output buffering*/
    setbuf(stdout, NULL);
}

We will also declare the needed storage for our program. Again the use of each of these character pointers, arrays, ect will be explained when they are used. Please note that all the following code will exist inside of our main function brackets!

Here we have two boolean values:

  1. quit – This boolean value is set to false when you want your program to continue. Think of it as “If quit is true, then quit the program.”
  2. display_shell – When this boolean value is true, we will display the name of our shell. In this case our shell will be called the MicroShell. There are many classic examples of shell names such as BASH (Bourne Again Shell).
    /*If quit flag is true then end the shell*/
    while(!quit)
    {
        /*Initial shell name*/
        if(display_shell)
        {   
            printf("MicroShell>");
            display_shell = FALSE;
        }
    }

Within our while loop and after our display shell statement, we are going to add another while loop used to read in a single command entered into our shell. The code looks like the following

while (fgets(buf,1024,stdin) != NULL)
{
 
}

Lets explain how the fgets() function works.

  1. Think of fgets() as “Get a string from a file stream”.
  2. The first argument passed into fgets is a buffer used to store the string that is being read.
  3. A buffer is just a temporary location in memory where you wish to store some data. We previously declared our buffer as a space which will hold 1024 bytes of data, which is the same as storing up to 1024 characters, because each character is 1 byte. Then we defined or filled our buffer with \0. This is the equivalent of setting our buffer to blank values.
  4. The second argument tells the fgets() how big our buffer is. In our case, 1024 bytes.
  5. The third argument passed in is a symbol called stdin which stands for standard input. Whenever a program needs to read data or write data to some memory location, you must tell the program where it will read/write too. In Linux we use a number called a file descriptor to determine where we are reading or writing from. The number 0 is the very first file descriptor which happens to be the same value as stdin. This basically means, read a string from standard input, which in our case happens to be the terminal we are entering the command into. If you wanted our program to write data to terminal (instead of reading), you could use stdout or standard output. stdout happens to be the same as the file descriptor 1. You can also write any errors that may occur to stderr, which is file descriptor 2. You can also create your own additional file descriptors from 3 onward, which can identify files that you want to read or write to.
  6. Think of this statement as “Read an data entered into standard input (our terminal) and store this data as string inside of buf“.
Posted in Programming, Projects.