Build your own Shell : PART 2 👨🏾‍💻
Tags: codenewbie, c, tutorial, beginners
Published: 2023-11-06 09:01:09

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](https://dev.to/angelotheman/build-your-own-shell-part-1-1192)

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 */
```

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);
}
```
Don't forget we would compile with this command

```
gcc -Wall -Wextra -pedantic *.c -o shell && ./shell
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sqfqunipiem07nudmdb1.png)

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
```

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);
}
```

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](https://linux.die.net/man/3/getenv)

**Synopsis**
```
#include <stdlib.h>

char *getenv(const char *name);
```
**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);
}
```

#### 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
```
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 */
```
#### 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](https://linux.die.net/man/2/stat)

**Synopsis**
```
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
```
**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](https://linux.die.net/man/2/access)

**Synopsis**
```
#include <unistd.h>

int access(const char *pathname, int 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](https://linux.die.net/man/3/malloc)) memory for that.
2. We would have to duplicate using `strdup` ([man 3 strdup](https://linux.die.net/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](https://linux.die.net/man/3/strdup)).

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);
}
```
#### 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 */
```
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);
}
```
#### 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);
}
```
#### 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 */
```
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);
}
```
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 */
```
Let's compile our file with the command 
```
gcc -Wall -Werror -Wextra -pedantic *.c -o hsh && ./hsh
```

![Result's as at now](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ksjxcsmuhzbguo0s75ux.png)

## Extras
See what happens when you enter the following command in the terminal 
```echo "ls" | ./hsh```

![Result of command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oumbgq5cddikb1xwg3uv.png)

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](https://linux.die.net/man/3/isatty)

**Synopsis**:
```
#include <unistd.h>

int isatty(int fd);
```
**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);
}
```
#### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxdls74rusnyj8999slg.png)

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);
}
```
`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 */
```
`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);
}
```
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](https://www.github.com/angelotheman), let's get interactive on [Twitter](https://www.twitter.com/_angelotheman) and form great connections on [LinkedIn](https://www.linkedin.com/in/angelotheman) 😊

Happy coding 🥂