DEV Community

Angel Oduro-Temeng Twumasi
Angel Oduro-Temeng Twumasi

Posted on

Build your own Shell : PART 2 πŸ‘¨πŸΎβ€πŸ’»

In the first part of our journey to create a shell, we explored the basics, crafting a shell that worked when we typed in commands but needed the complete path, like /bin/ls, to function properly.

Find the previous article HERE

Now, in this next step, we're going to upgrade our shell to be more flexible and user-friendly:

We'll make our shell more versatile, allowing it to be used in both interactive chats and automated processes.
We're going to tweak things so that the shell understands simple commands like ls without needing the whole path, just like we're used to in regular shells.
In a nutshell, we're enhancing our shell to be easier to use and adaptable to different situations. This way, it'll feel more like the shells we commonly use, making our coding journey more exciting and practical.

Linus Torvalds once said and I want you to note πŸ‘‡πŸΎ

"Talk is cheap. Show me the code" - Linus Torvalds

Now let's show the code πŸ”₯

This is our current main.h file

#ifndef MAIN_H
#define MAIN_H

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>

#endif /* MAIN_H */
Enter fullscreen mode Exit fullscreen mode

and this is our current main.c file

#include "main.h"

/**
  * main - Getline function
  * @argc: Argument count
  * @argv: Array of argument values
  *
  * Return: 0 on success
  */

int main(int argc, char **argv)
{
        (void)argc, (void)argv;
        char *buf = NULL, *token;
        size_t count = 0;
        ssize_t nread;
        pid_t child_pid;
        int i, status;
        char **array;

        while (1)
        {
                write(STDOUT_FILENO, "MyShell$ ", 9);

                nread = getline(&buf, &count, stdin);

                if (nread ==  -1)
                {
                        perror("Exiting shell");
                        exit(1);
                }

                token = strtok(buf, " \n");

                array = malloc(sizeof(char*) * 1024);
                i = 0;

                while (token)
                {
                        array[i] = token;
                        token = strtok(NULL, " \n");
                        i++;
                }

                array[i] = NULL;

                child_pid = fork();

                if (child_pid == -1)
                {
                        perror("Failed to create.");
                        exit (41);
                }

                if (child_pid == 0)
                {
                        if (execve(path, array, NULL) == -1)
                        {
                                perror("Failed to execute");
                                exit(97);
                        }
                }
                else
                {
                        wait(&status);
                }
        }
        free(buf);
        return (0);
}
Enter fullscreen mode Exit fullscreen mode

Don't forget we would compile with this command

gcc -Wall -Wextra -pedantic *.c -o shell && ./shell
Enter fullscreen mode Exit fullscreen mode

In order to enhance our shell functionality so that it would work with just the executables eg ls, I would create a function in a file to handle that.

In the original shell, when the user inputs ls, here's what happens

The shell looks through the PATH environment variable. The PATH is made up of various directories which contain various executable files. These directories are where the system searches for executable files. When you type a command in the terminal, the system goes through these directories in order until it finds the executable file corresponding to the command you entered. If it doesn't find the command in any of these directories, it will display a "command not found" error.

Image of using the PATH

These are the directories in my PATH

/home/vagrant/.local/bin:/home/vagrant/.local/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
Enter fullscreen mode Exit fullscreen mode

Note that they are separated by a colon :. Hence, in effect, when the user enters ls, the shell builds an executable path which would be /bin/ls and runs that instead.

With this understanding, we need to upgrade our shell with this functionality.

Here are the steps that we would take

STEP 1: We would get the PATH variable with the getenv() command.
STEP 2: Tokenize each directory in the PATH with strtok()
STEP 3: For each directory, we build an absolute path for the file (user input). Hence if the user input is ls or hello, we build an absolute path like so /usr/local/bin/ls or /usr/local/bin/hello respectively assuming /usr/local/bin/ is our first directory in the PATH environment.
STEP 4: After constructing the absolute path above, stat() is to check the existence of that file in that directory and gather as much information about that file. We then access() to check if the file is accessible and executable. If either stat() or access() fails, then the file is either not found or not executable.
With the example above, /usr/local/bin/hello is invalid whereas /usr/local/bin/ls is valid.
STEP 5: The first instance of the executable file is returned to the execve(). We would use a function get_file_path for this and assign its value to a variable.

NOTE: The first argument (array[0]) of the execve() function is the absolute path of the executable.

Add this code to the main.c file

/* Initialize the path variable */
char *path;

/* Add this code just after array[i] == NULL */
path = get_file_path(array[0]);

/* Now update the execve to use the path variable like so */
if (execve(path, array, NULL) == -1)
{
    perror("Failed to execute")
    exit(97);
}
Enter fullscreen mode Exit fullscreen mode

Now we would code the get_file_path() function in a different file. Create a file file_loc.c.

In this file, I would add a helper function to the get_file_path() named get_file_loc(). get_file_path() would retrieve the PATH variable, check whether the PATH exists. If yes, the value of PATH is passed to the get_file_loc() function would return the full path of the file to the get_file_path.

To get the PATH as stated in STEP 1 above, we use the getenv() function.

Getenv()

man 3 getenv

Synopsis

#include <stdlib.h>

char *getenv(const char *name);
Enter fullscreen mode Exit fullscreen mode

Description: The getenv() function searches the environment list to find the environment variable name.

Return Value: A pointer to the corresponding value of name or NULL if there's no match.

In our file_loc.c file, let's add the get_file_path function.

#include "main.h"

/**
  * get_file_path - Get's the full path of the file
  * @file_name: Argument name
  *
  * Return: The full path argument to the file
  */

char *get_file_path(char *file_name)
{
        char *path = getenv("PATH");

        if (!path)
        {
                perror("Path not found");
                return (NULL);
        }

        return (path);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • The get_file_path takes an argument file_name which would be the input of the user or array[0] used by the execve.
  • The variable path stores the value of the getenv("PATH").
  • A condition is checked for the value returned by the getenv. If false, the condition is executed ie. The "PATH" value wasn't found.
  • We then return the value.

NOTE: The value we are returning would be something like this

/home/vagrant/.local/bin:/home/vagrant/.local/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
Enter fullscreen mode Exit fullscreen mode

which is not exactly what we need.

Hence, we would create another function to help us tokenize this value as stated in STEP 2. After which the same function would help us build an absolute path with the file name which in our case is ls.

Add this code to the get_file_path function

/* Add to the initialization */
char *full_path;


/* Add this to the function body */
full_path = get_file_loc(path, file_name);

if (full_path == NULL)
{
       perror("Absolute path not found");      
       return (NULL);
}

return (full_path); /* Replace the current return with this */
Enter fullscreen mode Exit fullscreen mode

Explanation

  • We pass the path value and the file_name (ls) into the get_file_loc function. This function would help us build the absolute path for the filename ls after tokenizing the path value.
  • The result is stored in the full_path variable.
  • We check if it's null and return NULL if Yes, otherwise we return the absolute path to the function.

Now in the get_file_loc function,

  1. We tokenize the path as stated in STEP 2 with the delimeter :.
  2. For each directory, we build an absolute path for and append the file_name to the end of it.
  3. We then use stat and access as stated in the STEPS above to check the existence and execute permissions of the file

Stat

man 2 stat

Synopsis

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
Enter fullscreen mode Exit fullscreen mode

Description: Returns the information about the file in the buffer pointed to by the statbuf variable.

Return Value: On success 0, on error -1

Access

man 2 access

Synopsis

#include <unistd.h>

int access(const char *pathname, int mode);
Enter fullscreen mode Exit fullscreen mode

Description: This checks whether the file pathname can be accessed. It has three modes:
- F_OK: This tests for the existence of the file
- R_OK: Tests if file exists and grants read permissions
- W_OK: Tests if file exists and grants write permissions
- X_OK: Tests if file exists and grants execute permissions

Return Value: 0 if all permissions (modes) specified are met or -1 if at least one fails.

With this well settled there are other things to consider.

  1. We would create a buffer to hold the absolute path, hence, we would have to malloc (man 3 malloc) memory for that.
  2. We would have to duplicate using strdup (man 3 strdup) the value from the PATH so that we do not loose it during tokenization.
  3. We would also perform concatenation using strlen (man 3 strlen).

Now this is how the get_file_loc function would look like. Add this to the file file_loc.c.

/**
  * get_file_loc - Get the executable path of file
  * @path: Full path variable
  * @file_name: The executable file
  *
  * Return: Full path to the executable file
  */

char *get_file_loc(char *path, char *file_name)
{
        char *path_copy, *token;
        struct stat file_path;
        char *path_buffer = NULL;

        path_copy = strdup(path);
        token = strtok(path_copy, ":");

        while (token)
        {
                if (path_buffer)
                {
                        free(path_buffer);
                        path_buffer = NULL;
                }
                path_buffer = malloc(strlen(token) + strlen(file_name) + 2);
                if (!path_buffer)
                {
                        perror("Error: malloc failed");
                        exit(EXIT_FAILURE);
                }
                strcpy(path_buffer, token);
                strcat(path_buffer, "/");
                strcat(path_buffer, file_name);
                strcat(path_buffer, "\0");

                if (stat(path_buffer, &file_path) == 0 && access(path_buffer, X_OK) == 0)
                {
                        free(path_copy);
                        return (path_buffer);
                }
                token = strtok(NULL, ":");
        }
        free(path_copy);
        if (path_buffer)
                free(path_buffer);
        return (NULL);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • We initialize all variables that would be needed.
  • The struct stat file_path is a variable used by the stat() function. See it's implementation in the man page
  • We duplicate the original path and store that in the variable path_copy. This is done so that we don't loose the original PATH when manipulating it. strdup allocates memory and the copies the content of the PATH into path_copy.
  • The value of the PATH is then tokenized with the delimiter :.
  • path_buffer is used to store the absolute path of the executable file input from the user. We need to allocate memory for it and it's size would be the length of the token (eg /usr/bin), the length of file (eg ls) and then 2 (because we would add a / and then end it with a null character \0 to make it a complete string).
  • strcpy and strcat are used to build this absolute path which would look like /usr/bin/ls (notice that we added a / before the ls and then we completed it with the null character to make it a string).
  • After building the absolute path, we use stat to get more information about the file. We then use access with the mode X_OK to check if the file is executable. -When these conditions are satisfied, we would free the path_copy because we allocated space for it and then we return the absolute path which is the path_buffer. Otherwise, we continue the string tokenization.
  • If we didn't get a match for the executable file, we free the path_copy and path_buffer and return NULL.

That was a lot I know πŸ˜€, take your time to re-read it again with understanding

Don't forget to update your main.h file to include the headers of our new functions and system calls.
Here's our updated main.h

#ifndef MAIN_H
#define MAIN_H

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <string.h>

/* Helper Funcitons */
char *get_file_path(char *file_name);
char *get_file_loc(char *path, char *file_name);

#endif /* MAIN_H */
Enter fullscreen mode Exit fullscreen mode

Now we would add this to our main.c file.

#include "main.h"

/**
  * main - Getline function
  * @argc: Argument count
  * @argv: Array of argument values
  *
  * Return: 0 on success
  */

int main(int argc, char **argv)
{
        (void)argc, (void)argv;
        char *buf = NULL, *token, *path;
        size_t count = 0;
        ssize_t nread;
        pid_t child_pid;
        int i, status;
        char **array;

        while (1)
        {
                write(STDOUT_FILENO, "MyShell$ ", 9);

                nread = getline(&buf, &count, stdin);

                if (nread ==  -1)
                {
                        perror("Exiting shell");
                        exit(0);
                }

                token = strtok(buf, " \n");

                array = malloc(sizeof(char*) * 1024);
                i = 0;

                while (token)
                {
                        array[i] = token;
                        token = strtok(NULL, " \n");
                        i++;
                }

                array[i] = NULL;

                path = get_file_path(array[0]); /* Get the absolute path using the function */

                child_pid = fork();

                if (child_pid == -1)
                {
                        perror("Failed to create.");
                        exit (41);
                }

                if (child_pid == 0)
                {
                        if (execve(path, array, NULL) == -1) /* Replace the first argument with that executable file */
                        {
                                perror("Failed to execute");
                                exit(97);
                        }
                }
                else
                {
                        wait(&status);
                }
        }
        free(path);
        free(buf);
        return (0);
}
Enter fullscreen mode Exit fullscreen mode

NOTICE THE CHANGES ‼️

  1. We pass the first input of the user (array[0]) to the function get_file_path so that the absolute file path is returned to us.
  2. We then pass this to the execve function since it's first argument is supposed to be an executable file/command.

But there's a slight problem
We want our shell to handle user inputs whether it is the absoulute path (/usr/bin/ls) or just the executable file(ls).

We would implement a function to help us with that startsWithForwardSlash. Let's add this to the file_loc.c file.

/**
  * startsWithForwardSlash - Checks if file starts with "/"
  * @str: The filename to be checked
  *
  * Return: 0 if yes and 1 if NO
  */

int startsWithForwardSlash(const char *str)
{
        if (str != NULL || str[0] == '/')
                return (1);

        return (0);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

This function simply checks the first character of the input string and returns a 1 or 0 if found or not found respectively.

We would update our get_file_path function to check if the first character is a /.

/* Previous initializations */

if (startsWithForwardSlash(file_name) && access(file_name, X_OK) == 0)
         return (strdup(file_name));

/* Rest of code */
Enter fullscreen mode Exit fullscreen mode

Hence the full file_loc.c file would look like this

#include "main.h"

/**
  * startsWithForwardSlash - Checks if file starts with "/"
  * @str: The filename to be checked
  *
  * Return: 0 if yes and 1 if NO
  */

int startsWithForwardSlash(const char *str)
{
        if (str != NULL || str[0] == '/')
                return (1);

        return (0);
}

/**
  * get_file_loc - Get the executable path of file
  * @path: Full path variable
  * @file_name: The executable file
  *
  * Return: Full path to the executable file
  */

char *get_file_loc(char *path, char *file_name)
{
        char *path_copy, *token;
        struct stat file_path;
        char *path_buffer = NULL;

        path_copy = strdup(path);
        token = strtok(path_copy, ":");

        while (token)
        {
                if (path_buffer)
                {
                        free(path_buffer);
                        path_buffer = NULL;
                }
                path_buffer = malloc(strlen(token) + strlen(file_name) + 2);
                if (!path_buffer)
                {
                        perror("Error: malloc failed");
                        exit(EXIT_FAILURE);
                }
                strcpy(path_buffer, token);
                strcat(path_buffer, "/");
                strcat(path_buffer, file_name);
                strcat(path_buffer, "\0");

                if (stat(path_buffer, &file_path) == 0 && access(path_buffer, X_OK) == 0)
                {
                        free(path_copy);
                        return (path_buffer);
                }
                token = strtok(NULL, ":");
        }
        free(path_copy);
        if (path_buffer)
                free(path_buffer);
        return (NULL);
}

/**
  * get_file_path - Get's the full path of the file
  * @file_name: Argument name
  *
  * Return: The full path argument to the file
  */

char *get_file_path(char *file_name)
{
        char *path = getenv("PATH");
        char *full_path;

        if (startsWithForwardSlash(file_name) &&
                        access(file_name, X_OK) == 0)
                return (strdup(file_name));

        if (!path)
        {
                perror("Path not found");
                return (NULL);
        }

        full_path = get_file_loc(path, file_name);

        if (full_path == NULL)
        {
                write(2, file_name, strlen(file_name));
                write(2, ": command not found\n", 19);
                return (NULL);
        }

        return (full_path);
}
Enter fullscreen mode Exit fullscreen mode

Finally, we update our main.h file with the new function

#ifndef MAIN_H
#define MAIN_H

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <string.h>

/* Helper Funcitons */
char *get_file_path(char *file_name);
char *get_file_loc(char *path, char *file_name);
int startsWithForwardSlash(const char *str);


#endif /* MAIN_H */
Enter fullscreen mode Exit fullscreen mode

Let's compile our file with the command

gcc -Wall -Werror -Wextra -pedantic *.c -o hsh && ./hsh
Enter fullscreen mode Exit fullscreen mode

Result's as at now

Extras

See what happens when you enter the following command in the terminal

echo "ls" | ./hsh

Result of command

However we want this to execute the ls command against the ./hsh executable file. This is known as non-interactive mode.
We would use the isatty command for this.

Isatty

man 3 isatty

Synopsis:

#include <unistd.h>

int isatty(int fd);
Enter fullscreen mode Exit fullscreen mode

Description:
The isatty() function tests whether fd is an open file descriptor referring to a terminal. This means it checks for the file descriptors - in our case, 0 or STDIN_FILENO, which checks for a file or standard input - and works accordingly.
Return Value:
isatty() returns 1 if fd is an open file descriptor referring to a terminal; otherwise 0 is returned, and errno is set to indicate the error.

Let's add the isatty() function to our main.

/* Add it after the while condition */
if (isatty(STDIN_FILENO))
     write(STDOUT_FILENO, "MyShell$ ", 9);

/* Modify the nread function like so */
if (nread == -1)
{
     exit(1);
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • The isatty function works to check whether the file is opened in interactive or non-interactive mode using the file descriptor
  • We removed the perror message in the nread condition of the getline function. This is so that we don't get such error messages when exiting the program in either interactive or non-interactive modes.

Now you should get the following output

Result of the function

Here's a summary of all the files
main.c

#include "main.h"

/**
  * main - Getline function
  * @argc: Argument count
  * @argv: Array of argument values
  *
  * Return: 0 on success
  */

int main(int argc, char **argv)
{
        (void)argc, (void)argv;
        char *buf = NULL, *token, *path;
        size_t count = 0;
        ssize_t nread;
        pid_t child_pid;
        int i, status;
        char **array;

        while (1)
        {
                if (isatty(STDIN_FILENO))
                        write(STDOUT_FILENO, "MyShell$ ", 9);

                nread = getline(&buf, &count, stdin);

                if (nread ==  -1)
                {
                        exit(0);
                }

                token = strtok(buf, " \n");

                array = malloc(sizeof(char*) * 1024);
                i = 0;

                while (token)
                {
                        array[i] = token;
                        token = strtok(NULL, " \n");
                        i++;
                }

                array[i] = NULL;

                path = get_file_path(array[0]);

                child_pid = fork();

                if (child_pid == -1)
                {
                        perror("Failed to create.");
                        exit (41);
                }

                if (child_pid == 0)
                {
                        if (execve(path, array, NULL) == -1)
                        {
                                perror("Failed to execute");
                                exit(97);
                        }
                }
                else
                {
                        wait(&status);
                }
        }
        free(path);
        free(buf);
        return (0);
}
Enter fullscreen mode Exit fullscreen mode

main.h

#ifndef MAIN_H
#define MAIN_H

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <string.h>

/* Helper Funcitons */
char *get_file_path(char *file_name);
char *get_file_loc(char *path, char *file_name);
int startsWithForwardSlash(const char *str);


#endif /* MAIN_H */
Enter fullscreen mode Exit fullscreen mode

file_loc.c

#include "main.h"

/**
  * startsWithForwardSlash - Checks if file starts with "/"
  * @str: The filename to be checked
  *
  * Return: 0 if yes and 1 if NO
  */

int startsWithForwardSlash(const char *str)
{
        if (str != NULL || str[0] == '/')
                return (1);

        return (0);
}

/**
  * get_file_loc - Get the executable path of file
  * @path: Full path variable
  * @file_name: The executable file
  *
  * Return: Full path to the executable file
  */

char *get_file_loc(char *path, char *file_name)
{
        char *path_copy, *token;
        struct stat file_path;
        char *path_buffer = NULL;

        path_copy = strdup(path);
        token = strtok(path_copy, ":");

        while (token)
        {
                if (path_buffer)
                {
                        free(path_buffer);
                        path_buffer = NULL;
                }
                path_buffer = malloc(strlen(token) + strlen(file_name) + 2);
                if (!path_buffer)
                {
                        perror("Error: malloc failed");
                        exit(EXIT_FAILURE);
                }
                strcpy(path_buffer, token);
                strcat(path_buffer, "/");
                strcat(path_buffer, file_name);
                strcat(path_buffer, "\0");

                if (stat(path_buffer, &file_path) == 0 && access(path_buffer, X_OK) == 0)
                {
                        free(path_copy);
                        return (path_buffer);
                }
                token = strtok(NULL, ":");
        }
        free(path_copy);
        if (path_buffer)
                free(path_buffer);
        return (NULL);
}

/**
  * get_file_path - Get's the full path of the file
  * @file_name: Argument name
  *
  * Return: The full path argument to the file
  */

char *get_file_path(char *file_name)
{
        char *path = getenv("PATH");
        char *full_path;

        if (startsWithForwardSlash(file_name) &&
                        access(file_name, X_OK) == 0)
                return (strdup(file_name));

        if (!path)
        {
                perror("Path not found");
                return (NULL);
        }

        full_path = get_file_loc(path, file_name);

        if (full_path == NULL)
        {
                write(2, file_name, strlen(file_name));
                write(2, ": command not found\n", 19);
                return (NULL);
        }

        return (full_path);
}
Enter fullscreen mode Exit fullscreen mode

Congratulations πŸ‘πŸΎ you have enhanced your shell. Go ahead and test your shell extensively πŸ”₯.

In our next article, we would implement other functionalities like exit, cd, handling #, env etc.

Follow me on Github, let's get interactive on Twitter and form great connections on LinkedIn 😊

Happy coding πŸ₯‚

Top comments (2)

Collapse
 
jacques00077 profile image
jacques00077

Thanks so much it has been a great help so when is the part 3 coming out?

Collapse
 
angelotheman profile image
Angel Oduro-Temeng Twumasi

Wow, I'll try and bring in a part 3 where we deal into more details