Creating a Terminal shell - Adding Builtins

This post is in continuation to to our series on creating a terminal shell. You can checkout the first post here. In this post, I’ll show how to add builtin commands to our shell.

Specifically, we’ll be tackling the cd builtin (A command provided by the shell). Now first let’s clear the air, why the heck is cd not a program ? So what is the primary purpose of the command cd ? To change the current working directory right ? But for whom if I may ask ? Well, in this case the terminal shell itself. So, if cd were to be a program (which it is in some POSIX compliant systems), whenever we run cd, it would change the current working directory of the cd process itself, and when the process would terminate, and return to shell, the shell would still have the same current working directory

This is how it would look :

shell $ pwd
/home/blog
shell $ cd_process /some/other/directory
    cd_process starts
        chdir (/some/other/directory) - System Call
        current working directory change successful
    cd_process terminates
shell $ pwd
/home/blog

Therefore, to get around this conundrum, the shell itself has call the chdir system call to be able to change its current working directory. The code for doing that is quite simple :

static int cd(const char *cmd, const char **args) {
if (!cmd) return -1;
int ret = chdir(args[1]);
return ret;
}
view raw bt.c hosted with ❤ by GitHub

Yes, that’s it. Now just for the sake of it, and to make it easier to add more builtins in the future, we’ll add some code to our existing shell to figure out whether it’s dealing with a builtin command or some other program.

First we’ll create a new header file inside our include directory builtin.h which will house the declarations for common functions, and also declare an enum which will correspond to all the builtins we’ll add to our shell. Right now it’ll have only two entries, one for cd, and one for null check.

#ifndef __BUILTINS_H_
#define __BUILTINS_H_
typedef enum {
BLTIN_CD,
BLTIN_NONE
} BUILTIN_CMD;
BUILTIN_CMD get_builtin(const char *cmd);
int call_builtin(BUILTIN_CMD cmd, const char **args);
#endif
view raw builtin.h hosted with ❤ by GitHub

Now we’ll create a series of utility functions which will help in translating the string “cd” that we’ll get from command line to the enum BLTIN_CD, to the actual function to be called. These functions are not really necessary right now, but it really helps to create them right in the beginning. In the future, when you add more builtin commands to your shell, it will be way more easier to do so.

The first function in our list takes a command string and returns an enum, this is the easiest of the bunch, we just compare the input string to predefined string list and return the corresponding enum.

BUILTIN_CMD get_builtin(const char *cmd) {
if (!cmd) return BLTIN_NONE;
// successively check for each BUILTIN
if (strcmp(cmd, "cd") == 0) return BLTIN_CD;
return BLTIN_NONE;
}
view raw tm6.c hosted with ❤ by GitHub

Now the next function is getting the actual function pointer from the builtin enum. Now don’t get scared of the syntax you’re going to see below. A function pointer is just a pointer to a function, and using that pointer we can call functions. Now there is a weird way to write functions which return a function pointer, I never remember it, just google when need to :P

static int (*get_func_ptr(BUILTIN_CMD cmd))(const char*, const char**) {
switch (cmd) {
case BLTIN_CD:
return &cd;
default:
return NULL;
}
}
view raw tm7.c hosted with ❤ by GitHub

Now the only function left is calling our function through the function pointer, which is not very difficult.

int call_builtin(BUILTIN_CMD cmd, const char **args) {
int (*func_ptr)(const char*, const char**) = get_func_ptr(cmd);
if (!func_ptr) return 1;
int ret = func_ptr(args[0], args);
return ret;
}
view raw tm8.c hosted with ❤ by GitHub

In the end we’ll have the following file, save this as src/builtins.c

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include "builtins.h"
static int cd(const char*, const char **);
static int (*get_func_ptr(BUILTIN_CMD cmd))(const char*, const char**) {
switch (cmd) {
case BLTIN_CD:
return &cd;
default:
return NULL;
}
}
BUILTIN_CMD get_builtin(const char *cmd) {
if (!cmd) return BLTIN_NONE;
// successively check for each BUILTIN
if (strcmp(cmd, "cd") == 0) return BLTIN_CD;
return BLTIN_NONE;
}
int call_builtin(BUILTIN_CMD cmd, const char **args) {
int (*func_ptr)(const char*, const char**) = get_func_ptr(cmd);
if (!func_ptr) return 1;
int ret = func_ptr(args[0], args);
return ret;
}
static int cd(const char *cmd, const char **args) {
if (!cmd) return -1;
int ret = chdir(args[1]);
return ret;
}
view raw builtins.c hosted with ❤ by GitHub

Now we just need to call our function inside our eval function. (Don’t forget to include builtins.h)

BUILTIN_CMD cmd = get_builtin(tokens[0]);
if (cmd != BLTIN_NONE) {
call_builtin(cmd, (const char**)tokens);
free(input_dup);
free(tokens);
return 0;
}
view raw tm9.c hosted with ❤ by GitHub

The final thing to do is change our Makefile to start building our builtins.c file. Just add $(LIB_DIR)/builtins.o to the OBJS variable

OBJS=$(LIB_DIR)/shell.o $(LIB_DIR)/builtins.o

Now just build, and we can cd into directories

kartik@kt:~/projects/blog$ ./bin/msh
msh >> pwd
/hdd/projects/blog
msh >> cd /
msh >> pwd
/

In the next post we’ll add the support for redirecting streams, so hold on!