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 */
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.
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()
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 argumentfile_name
which would be the input of the user or array[0] used by theexecve
. - The variable
path
stores the value of thegetenv("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 theget_file_loc
function. This function would help us build the absolute path for the filenamels
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,
- We tokenize the path as stated in STEP 2 with the delimeter
:
. - For each directory, we build an absolute path for and append the file_name to the end of it.
- We then use
stat
andaccess
as stated in the STEPS above to check the existence and execute permissions of the file
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
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.
- We would create a buffer to hold the absolute path, hence, we would have to
malloc
(man 3 malloc) memory for that. - We would have to duplicate using
strdup
(man 3 strdup) the value from the PATH so that we do not loose it during tokenization. - 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);
}
Explanation
- We initialize all variables that would be needed.
- The
struct stat file_path
is a variable used by thestat()
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 intopath_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 (egls
) and then 2 (because we would add a/
and then end it with a null character\0
to make it a complete string). -
strcpy
andstrcat
are used to build this absolute path which would look like/usr/bin/ls
(notice that we added a/
before thels
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 useaccess
with the modeX_OK
to check if the file is executable. -When these conditions are satisfied, we wouldfree
the path_copy because we allocated space for it and then we return the absolute path which is thepath_buffer
. Otherwise, we continue the string tokenization. - If we didn't get a match for the executable file, we
free
thepath_copy
andpath_buffer
and returnNULL
.
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 βΌοΈ
- 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. - 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
Extras
See what happens when you enter the following command in the terminal
"ls" | ./hsh
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
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 thenread
condition of thegetline
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
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, let's get interactive on Twitter and form great connections on LinkedIn π
Happy coding π₯
Top comments (2)
Thanks so much it has been a great help so when is the part 3 coming out?
Wow, I'll try and bring in a part 3 where we deal into more details