Creating a Simple Shell in Linux & Unix Environment

Home / Creating a Simple Shell in Linux & Unix Environment

In computer science especially in Unix world, a shell is a software that provides an interface for users to access kernel services. The name shell originates from shells being an outer layer of interface between user and the internals of operating system (kernel). Some shell we might know are: bash, csh, tcsh, sh, etc.

In this article, I will show you how to build a simple shell. Our shell can execute command which is inputted by user. But before that, we will cover some basic knowledge about how things going on.

How can our shell execute a command? The answer lies on the capability of exec() function family. Exec will execute command given by them and passing arguments if any. As I said exec() family functions, the actual function we can use are: execl(), execlp(), execle(), execv(), execvp().

How to distinguish between these function?

The suffix ‘l’ (el) means exec commands expect a null-terminated list (hence the “l” for list) of arguments, so to execute “/bin/ls/ -l /” we will call

execl("/bin/ls", "-l" , "/" , (char*) 0);

The suffix ‘v’ (vi) means exec commands expect an array or vector (hence the “v” for vector) of arguments. Here is the snippet for doing same thing as previous:

char *vec[4];

vec[0]="ls";
vec[1]="-l";
vec[2]="/";
vec[3]=(char *)0;
execv("/bin/ls", vec);

The suffix ‘e’ take an additional argument which is an array of char containing the environment (hence the ‘e’ for environment) to be passed to the exec-ed command. Both le and and ve expect an array.

The suffix ‘p’ search the PATH for the command named in their first argument. The one without p don’t.

To use them we should include <unistd.h> header.

In this article we will use execvp() to execute our command.

Now to do exec() thing, we will need help from fork(). What is fork? In computer science, a process do fork will creates a copy of itself. Under Unix OS, the parent and the child process can tell each other apart by examining the return value of the fork() system call. In the child process, the return value of fork() is 0 while the parent side has PID of newly created child process as return value.

Why we should create a fork? First, when exec has finish executing command, it would be exited by default. Our shell must remain running even after the command finished unless we are ordered to exit. Second, having ability to “background”-ing a process executed. You would noticed that when we enter a command in shell like bash and end it with &, our newly executed command would run independently while the shell itself is ready to receive new command.

Here is the complete source code for our shell:

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

const int BUFFER_SIZE = 1<<16;
const int ARR_SIZE = 1 << 16;
/* parse arguments and creating a vector of argument */
void parse_args(char* buffer, char** args, size_t args_size, size_t *nargs);

/* SIGNAL HANDLER */
/* handler for SIGCHLD */
void child_handler(int);

/* handler for SIGINT */
void interrupt_handler(int);

int child_count = 0;

int main(int argc, char* argv[]) {
   char buffer[BUFFER_SIZE];
   char* args[ARR_SIZE];
   int ret_status;
   size_t nargs;
   pid_t pid;
   int background;
   signal(SIGCHLD, &child_handler);
   signal(SIGINT, &interrupt_handler);
   while(1) {
      background = 0;
      printf("$> ");
      fgets(buffer, BUFFER_SIZE, stdin);
      parse_args(buffer, args, ARR_SIZE, &nargs);
      if(nargs==0) continue;
      if(!strcmp(args[0],"exit")) {
         printf("Session terminated\n");
         exit(0);
      }
      if( !strcmp(args[nargs-1], "&") ) {
         background = 1;
         args[nargs-1] = NULL;
         nargs--;
      }
      child_count += 1;
      pid = fork();
      if(pid > 0) {
      /* PARENT */
      if( !background ) {
         pid = wait(&ret_status);
         printf("Child (%d) finished\n", pid);
      } else {
         printf("[ ] %d\n", pid);
      }
   } else if(pid == 0) {
      /* CHILD */
      if( execvp(args[0], args) ) {
         puts(strerror(errno));
         exit(127);
      }
   } else {
      printf("Error: Can not fork\n");
      exit(1);
      }
   }
   return 0;
}

void parse_args(char* buffer, char** args, size_t args_size, size_t *nargs) {
   if( strlen(buffer) > 0 ) {
      char *buf_args[args_size];
      char **cp;
      char *wbuf;
      size_t i,j;
      wbuf=buffer;
      buf_args[0] = buffer;
      args[0] = buffer;
      for(cp=buf_args; (*cp=strsep(&wbuf," \n\t")) != NULL;) {
         if((*cp != '\0') && (++cp >= &buf_args[args_size]))
         break;
      }
      for(j=i=0; buf_args[i] != NULL; i++) {
         if(strlen(buf_args[i]) > 0)
         args[j++]=buf_args[i];
      }
      *nargs=j;
      args[j]=NULL;
   } else {
      *nargs = 0;
   }
}

/* handler for SIGCHLD */
void child_handler(int sig_type) {
   int child_status;
   pid_t child;
   /* Getting exit status from child */
   child = waitpid(0, &child_status, 0);
}

/* handler for SIGINT */
void interrupt_handler(int sig_type) {
   printf("\n%d child(s) has been spawned successfully. Session terminated\n", child_count);
   exit(0);
}

Any comment or question is welcomed.

, ,

About Author

about author

xathrya

A man who is obsessed to low level technology.

Leave a Reply

Your email address will not be published. Required fields are marked *

Social media & sharing icons powered by UltimatelySocial