Building a Shell from Scratch

Hello and welcome readers!! So this is the first post on my blog. And To kick things off, let's see how we can build a basic implementation of a shell. If you have ever wondered how a shell actually works, you can get to know it here.

Reading the Lines

First, we declare a buffer size (let's say 1024; it can be anything really but just big enough to hold the input), a variable called line that points to the array, a variable called position that indicates the current index, and an integer variable c.

We then use malloc to dynamically allocate the array. We dynamically allocate it since we can't predefine a definite number of characters from the start.

Next, we enter a loop and input characters through c = getchar(). If it's EOF or '\n', we end the loop, put line[position] in the array, end the loop, and return the pointer to the array. Otherwise, we store that character in the array and increase the position by 1. If the size of the array exceeds buffsize, we use realloc to reallocate the array.


                char *readline()
                {
                    int status;
                    char *line;
                    int buff_size = LSH_RL_BUFSIZE;
                    int position = 0;
                    int c;
                    line = (char *)malloc(buff_size * sizeof(char));
                    if (!line)
                    {
                        printf("No space allocated");
                        return NULL;
                    }
            
                while(1)
                {
                c=getchar();
                if(c=='\n'||c==EOF)
                {
                    line[position]='\0';
                    break;
                }
                else if(position>=buff_size)
                {
                    buff_size+=LSH_RL_BUFSIZE;
                    line=realloc(line,sizeof(char)*buff_size);
                    if(!line)
                    {
                    printf("%s","Reallocation unsuccesful");
                    }
                }
                else
                {
                    line[position]=c;
                    position++;
                }
                }
                    line[position]='\0';
                    return line;
                }
            
            

Splitting the Line

After reading the line, we pass the pointer to a function splitline. Here we declare a variable buff_size to be 64. We declare ptrtokens and token as a pointer and another as the pointer to pointer variable. Next, we dynamically allocate the tokens variable as an array where each element will be a variable.

We then go through a loop where using the strtok function, we split the line array using delimiters, get the pointer that points to these split lines, and put it in the array of pointers. We increment pos by 1 and continue until token != NULL. We then return the pointer that points to the array of pointers, i.e., tokens.

Before executing it, we create an array of built-ins for the commands that will be executed. We also create a function that stores the addresses of the functions of the commands we execute.


                char **splitline(char *line)
                {
            
                    int buff_size = LSH_TOK_BUFSIZE;
                    char **tokens;
                    char *token;
                    int pos = 0;
                    tokens = malloc(sizeof(line) * buff_size);
                    if (!tokens)
                    {
                        fprintf(stderr, "Allocation unsuccesfull");
                        return NULL;
                    }
                    token = strtok(line, LSH_TOK_DELIM);
                    while (token != NULL)
                    {
                        tokens[pos] = token;
                        pos++;
            
                        if (pos >= buff_size)
                        {
                            buff_size+=LSH_TOK_BUFSIZE;
                            tokens = (char **)realloc(tokens, sizeof(line) * buff_size);
                            if (!tokens)
                            {
                                fprintf(stderr, "Allocation unsuccesfull");
                                return NULL;
                            }
                        }
                        token = strtok(NULL, LSH_TOK_DELIM);
                    }
                    tokens[pos] = '\0';
                    return tokens;
                }
            
            

Executing Commands

Now, let's discuss the execute function:


    int sh_execute(char **splitline)
    {
        if (splitline[0] == NULL)
        {
            return 1;
        }
        for (int i = 0; i < size(); i++)
        {
            if (strcmp(splitline[0], builtins[i]) == 0)
            {
                return (*builtins_fun[i])(splitline);
            }
        }
        return sh_launch(splitline);
    }
            

So as you can see over here,what the execute function does is go over traverse the builtins that we have , try seeing whether it matches with args[0] and if it does call the function.If however it does not find any builtins , it executes sh_launch.

Now there's a little bit explaining needed.A shell is itself a process.When a shell executes an external program, it creates a fork of that process , the new process runs while the shell being a process as well is present in the background using wait() to again start executing after the child has stopped executing.Now commands like cd,cat are being built right into the shell.Therefore when we run cd,we want the shell itself to change its directory and not a child process.Therefore you get either return (*builtins_fun[i])(splitline) which executes the builtin command or return sh_launch(splitline) which creates a completely new process.

Creating the directory

In the sh_cd function, we first check whether there's an argument given with the cd command. If not, we simply output the current directory the person is in. If there's an error, we give an error message and end the program. If we get a '..', we step back to the previous directory. Otherwise, we change directories as per the arguments. At all these times, we use the chdir function where var is the name of the directory variable and sizeof(MAX_PATH) is the size of the variable.


                int sh_cd(char **args)
                {
                    char* store;
                    if (strcmp(args[0],"cd")==0 && args[1]==NULL)
                    {
                    store=_getcwd(store,sizeof(MAX_PATH));
                    printf("%s ",store);
                    return 1;
                    }
                    else if(strcmp(args[1],"..")==0)
                    {
                       chdir("..");
                    }
            
                    else
                    {
            
                        if (chdir(args[1])!=0)
                        {
                            fprintf(stderr, "Oops");
                            printf("\n");
                            return 0;
                        }
            
                    }
                    store=_getcwd(store,sizeof(MAX_PATH));
                    printf("%s ",store);
                    return 1;
                }
            
            

Making a Directory

Next up, we go to sh_mkdir. This function helps us to create a directory. If there are no arguments, it gives an error. If the directory already exists or the path is not found, it throws an error. Otherwise, it uses the CreateDirectory function, which takes the argument and NULL to create the directory.

 
    int sh_mkdir(char **args)
    {
    int result;
    result=CreateDirectory(args[1],NULL);
    if(args[1]==NULL)
    {
        fprintf(stderr,"No directory created \n");
        return 0;
    }
    else if(result==0)
    {
    if(errno==ERROR_ALREADY_EXISTS)
    {
        fprintf(stderr,"Directory already present \n");
        return 0;
    }
    else if(errno==ERROR_PATH_NOT_FOUND)
    {
        fprintf(stderr,"Path not found \n");
        return 0;
    }
    }
    printf("Directory created \n");
    char* store;
    store=_getcwd(store,sizeof(MAX_PATH));
    printf("%s ",store);
    return 1;
    }

Help, CLS, and Exit

For getting a list of all the commands, we iterate over all the built-ins and print them. For exit and cls, there's not much to explain.



    int sh_help()
    {
        printf("\n");
        printf("HERE ARE THE COMMANDS \n");
        for (int j = 0; j < size(); j++)
        {
            printf("%s\n", builtins[j]);
            printf("\n");
        }
        char* store;
        store=_getcwd(store,sizeof(MAX_PATH));
        printf("%s ",store);
        return 1;
    }

    int sh_exit()
    {
        return 0;
    }
    int sh_clear()
    {
        system("cls");
        char* store;
        store=_getcwd(store,sizeof(MAX_PATH));
        printf("%s ",store);
        return 1;
    }
       

Changing the Color

This one is my favorite. We first print out the characters and numbers present. We then take one input for the background color and one for the foreground color. We join both of them with snprintf and use the system command. Let's say we use 1 as the background and E as the foreground, and we get the desired color scheme.


            int sh_color()
            {
                char col;
                char backcol;
                printf("Press any of the keys in the table for seting background and foreground color \n");
                printf("Here's the table \n");
                printf("Color id	Color	Color id	Color \n");
                printf( "1	        Aqua	A	        Light  Green \n");
                printf( "2	        Green	0	        Black \n");
                printf( "3	        Blue	9	        Light  Blue \n");
                printf( "4	        Red	    B	        Light  Aqua \n");
                printf( "5	        Purple	C	        Light  Red \n");
                printf( "6	        Yellow	D	        Light  Purple \n");
                printf( "7	        White	E	        Light  Yellow \n");
                printf( "8	        Gray	F	        Bright White \n");
                printf("Enter the background color:- ");
                scanf(" %c",&backcol);
                printf("Enter the foreground color:- ");
                scanf(" %c",&col);
                char command[50];
                snprintf(command,sizeof(command),"Color %c%c",backcol,col);
                system(command);
                char* store;
                store=_getcwd(store,sizeof(MAX_PATH));
                printf("%s ",store);
                return 1;
            }

Extracting Contents from a File

Lastly, we see how the cat command can be used to extract contents from a file into the console. As of now, this command only works on text files, but I will add functionality for other types of files as well. If there's no cat in args[0] or no file given in args[1], it's an error. Otherwise, we take args[1], use the fopen function in C, and pass the file pointer to fgets. What fgets does in this loop is read the text line from the console, which we print out until it reaches NULL.


    int sh_cat(char **args)
    {
       char *err="cat";
       char *contains=".txt";
        FILE *ptr;

        if(strcmp(args[0],"cat")!=0)
        {
            printf("%s","oops not a cat command");
            return 0;
        }
        char result[sizeof(args[1])];
        ptr=fopen(args[1],"r");
         if(ptr==NULL && strstr(args[1],contains))
        {
            ptr=fopen(args[1],"w");
        }
        else if(ptr==NULL)
        {
            printf("%s","Oops no file");
            return 0;
        }
        while (fgets(result,sizeof(LSH_RL_BUFSIZE),ptr)!=NULL)
        {
            printf("%s",result);
        }
        fclose(ptr);
        char *store;
        store=_getcwd(store,sizeof(MAX_PATH));
        printf("%s",store);
        return 1;
    }
        

Now reading all of these

Now putting readline,splitline and execute into a single function we get:-


    void read_all()
    {
        char *line;
        char **split;
        int status;
        printf("Welcome to the holy command line \n");

        do
        {
            printf(">>");
            line = readline();
            split = splitline(line);
            status= sh_execute(split);
        } while (status);

        free(line);
        free(split);
    }
        

Putting it all together


            #define LSH_RL_BUFSIZE 1024
            #define LSH_TOK_BUFSIZE 64
            #define LSH_TOK_DELIM " \t\r\n\a"
            #include 
            #include 
            #include 
            #include 
            #include 
            #include
            #include
            #include
            #include
            #include
            #include
            #include"shell.h"
            #include"test.c"
            const char *builtins[] = {"cd","mkdir","help", "exit","echo","cls","color","cat"};
            int (*builtins_fun[])(char **) =
                {
                    &sh_cd,
                    &sh_mkdir,
                    &sh_help,
                    &sh_exit,
                    &sh_echo,
                    &sh_clear,
                    &sh_color,
                    &sh_cat
        
            };
        
            int size()
            {
                return sizeof(builtins) / sizeof(char *);
            }
        
            int sh_cat(char **args)
            {
               char *err="cat";
               char *contains=".txt";
                FILE *ptr;
        
                if(strcmp(args[0],"cat")!=0)
                {
                    printf("%s","oops not a cat command");
                    return 0;
                }
                char result[sizeof(args[1])];
                ptr=fopen(args[1],"r");
                 if(ptr==NULL && strstr(args[1],contains))
                {
                    ptr=fopen(args[1],"w");
                }
                else if(ptr==NULL)
                {
                    printf("%s","Oops no file");
                    return 0;
                }
                while (fgets(result,sizeof(LSH_RL_BUFSIZE),ptr)!=NULL)
                {
                    printf("%s",result);
                }
                fclose(ptr);
                char *store;
                store=_getcwd(store,sizeof(MAX_PATH));
                printf("%s",store);
                return 1;
            }
            int sh_cd(char **args)
            {
                char* store;
                if (strcmp(args[0],"cd")==0 && args[1]==NULL)
                {
                store=_getcwd(store,sizeof(MAX_PATH));
                printf("%s ",store);
                return 1;
                }
                else if(strcmp(args[1],"..")==0)
                {
                   chdir("..");
                }
        
                else
                {
        
                    if (chdir(args[1])!=0)
                    {
                        fprintf(stderr, "Oops");
                        printf("\n");
                        return 0;
                    }
        
                }
                store=_getcwd(store,sizeof(MAX_PATH));
                printf("%s ",store);
                return 1;
            }
        
            int sh_mkdir(char **args)
            {
            int result;
            result=CreateDirectory(args[1],NULL);
            if(args[1]==NULL)
            {
                fprintf(stderr,"No directory created \n");
                return 0;
            }
            else if(result==0)
            {
            if(errno==ERROR_ALREADY_EXISTS)
            {
                fprintf(stderr,"Directory already present \n");
                return 0;
            }
            else if(errno==ERROR_PATH_NOT_FOUND)
            {
                fprintf(stderr,"Path not found \n");
                return 0;
            }
            }
            printf("Directory created \n");
            char* store;
            store=_getcwd(store,sizeof(MAX_PATH));
            printf("%s ",store);
            return 1;
            }
        
            int sh_help()
            {
                printf("\n");
                printf("HERE ARE THE COMMANDS \n");
                for (int j = 0; j < size(); j++)
                {
                    printf("%s\n", builtins[j]);
                    printf("\n");
                }
                char* store;
                store=_getcwd(store,sizeof(MAX_PATH));
                printf("%s ",store);
                return 1;
            }
        
            int sh_exit()
            {
                return 0;
            }
        
            int sh_echo(char**args)
            {
            char ch;
             if(args[1]==NULL)
            {
                printf("ECHO MODE ON!! BUT TYPE SOMETHING PLEASE");
            }
            else
            {
             int k=1;
              while(k=buff_size)
            {
                buff_size+=LSH_RL_BUFSIZE;
                line=realloc(line,sizeof(char)*buff_size);
                if(!line)
                {
                printf("%s","Reallocation unsuccesful");
                }
            }
            else
            {
                line[position]=c;
                position++;
            }
            }
                line[position]='\0';
                return line;
            }
        
        
            char **splitline(char *line)
            {
        
                int buff_size = LSH_TOK_BUFSIZE;
                char **tokens;
                char *token;
                int pos = 0;
                tokens = malloc(sizeof(line) * buff_size);
                if (!tokens)
                {
                    fprintf(stderr, "Allocation unsuccesfull");
                    return NULL;
                }
                token = strtok(line, LSH_TOK_DELIM);
                while (token != NULL)
                {
                    tokens[pos] = token;
                    pos++;
        
                    if (pos >= buff_size)
                    {
                        buff_size+=LSH_TOK_BUFSIZE;
                        tokens = (char **)realloc(tokens, sizeof(line) * buff_size);
                        if (!tokens)
                        {
                            fprintf(stderr, "Allocation unsuccesfull");
                            return NULL;
                        }
                    }
                    token = strtok(NULL, LSH_TOK_DELIM);
                }
                tokens[pos] = '\0';
                return tokens;
            }
        
            void read_all()
            {
                char *line;
                char **split;
                int status;
                printf("Welcome to the holy command line \n");
        
                do
                {
                    printf(">>");
                    line = readline();
                    split = splitline(line);
                    status= sh_execute(split);
                } while (status);
        
                free(line);
                free(split);
            }
        
            int main()
            {
                /*test_cls();
                test_grep();
                test_echo();
                test_cd();
                test_mkdir();
                test_help();
                test_exit();
                test_color();*/
                read_all();
            }
        

Conclusion

And that's the end of the blog.Here's the source code.You can check out the roadmap for this project over here. Until then, goodbye!