Hak5
Save 10% at GoDaddy.com with coupon code HAK

Analog5:002 Article 002

From Hak5

Jump to: navigation, search

Writing your own shell in C/C++

By: Nickisgod1
Published: March 20th, 2007


One might ask why, is there a need, is it useful? Well most likely the answer is no, It's not needed, and in the long run it's only use is that it introduces you to some very useful functions. However, It can be fun if you have nothing to do and it is a great learning experience. That being said, lets get a list of what we want our basic shell to do.

  1. have a prompt which includes the host name
  2. display the present working directory
  3. change the present working directory
  4. start other processes
  5. start other processes in the background
  6. basic piping.
  7. quit on a certain string, lets say "exit"

I think this a pretty basic list of what a shell should do at its most basic level, so lets get started. First lets build a simple frame that gets input, and stores it in a string or c style array. For this example well use a string, for no good reason other than i like them better. However, seeing as most of the functions we will use later are C functions it may be simpler and cleaner to use a C style array from the get go. OK, lets include the standard I/O functions and the string library to start with a simple while loop checking for exit, or a command

	#include <iostream>
	#include <string>
	using namespace std;
	int main(int argc, char *argv[])
	{
		string command; 
		do
		{
			cout<<"basic prompt: ";
			getline(cin,command);
		}while(command!="exit");

  	    return EXIT_SUCCESS;
	}

We now have a shell that display a prompt and waits for input, and quits upon the string exit. All thats needed now is to parse our input for use

	int main(int argc, char *argv[])
	{
		string command; 
		do
		{
			cout<<"basic prompt: ";
			getline(cin,command);
			string com; 					
                        //string to store switch

			while(command[command.size()-1] == ' ' ) 
                        // trim tailing whitespace

			{
				command.erase(command.size()-1);
			}
			string::size_type pos = command.find(' ');	
                        //find first command and isolate it

			// if statement  will find first command
			if (pos == string::npos)			
			{
				com=command;
			}
			else
			{
				com=command.substr(0,pos);
			}
		}while(command!="exit");

 	 return EXIT_SUCCESS;
	}

Now that we have a usable command it is time to start on our list. Many of the functions used here are available from the unistd library, so lets include that.

#include <unistd.h>

For the first entry on our list, we need to get the hostname and login name, this can easily be done though calls to the functions get_login_r(), and gethostname() used in something similar to this

char hostname[256]; gethostname(hostname, 256); char user[256]; getlogin_r(user, 256);

Then we could then change our standard prompt to read something like this

cout<<"["<<user<<"@"<<hostname<<"]$ ";

That solves list item one, now how about two, again there is a function available to us to achieve this, getcwd(), it is used in this fashion.

  	void pwdDisplay(){
		char pwd[256]; 
		getcwd(pwd, 256);
		cout<<pwd<<endl;
	}	

Now we know what the pwd is, but how should we display it? Lets call this our first built in function, we can either do this with if/else statements, or use enumeration and switch on the string, for the sake of simplicity we will use an if statement. if(com=="pwd") getpid()pwdDisplay(); A note to the reader, although not always included here most, if not all, of the functions I reference do set the errno, and return a negative value on an error, error checking is good. Dont be afraid to use it.

Now the program can return the present directory, but what if the users wishes to change it? This is the reason for our second built in function, well call it cd. the ready built function we can use is also included with unistd and is called chdir(), and takes a c style array as an argument, so assuming we are using strings, we can do something similiar to this

  	void change_dir( string command)
	{
		string dir; //string to store directory
		string::size_type pos = command.find(' '); 
                //find whitspace to look for directory

		if (pos == string::npos) //no space means no directory
		{
			cerr << "you must enter a directory" << endl;
		}
		else
		{
			pos++; //move onespace up
			dir=command.substr(pos,command.size()-pos); 
                        //create a substring of the directory

			if(chdir(dir.c_str())<0)    //change to that dir
			{
				perror("chdir");    //error
			}
		}
	}

Again don't forget your error checking and to add the function to your main loop with something like

	else if(com=="cd")
		{
			change_dir( command);
		}

Alright first 2 done, any other functionality that the programmer would like to add such as math ability, remembering history etc could be added here, but since this does not really introduce new functions I will leave that as an exercise to the reader. Now lets add the ability to start other processes. To create the new process we will use fork(), which creates a clone of the parent process, we can than replace the clone with our new process, for this we will use a member the execve family. We will use execvp as it searches the user path. For other examples in the family see man execvp and man execve. The function takes a c style array as the first argument (the process we wish to start) and an array of pointers(the process + its arguments). So again we need to parse our command, sense I am using strings, and we need to get it into an array of * char i used a string stream, but any way will work. We will also assume that if the command given is not one of our prebuilt functions, then it is an outside program, thus we will use the last else if in our loop to call this function.

  	else if(com != "exit")
	{prog_normal(command);} 

and the basic function

  void prog_normal(string command)
  {
	char argv2[10][128];	// c style to hold string to point at
	char  * argv[10]={NULL};	// array of pointers to char 
	string command1=command;	// strings to maipulate
	string command2;		
	stringstream ss(stringstream::in | stringstream::out); 
        // in out string stream for conversion

	string::size_type pos = command1.find(' ');	//find the first break
	string arg1=command1.substr(0,pos);		// this is the command
	command1.erase(0,pos+1);			//get rid of it
	int c=1;					// initialize counter
	ss.str(arg1);
	ss>>argv2[0];
	argv[0]=argv2[0];
	do
	{
		stringstream ss(stringstream::in | stringstream::out); 
                // in out string stream for conversion
		if(c>9)
		{
			cout<<"error:To many arguements"<<endl;
			break;
		}
		else if(pos == string::npos) // no args
		{
			break;
		}
		else
		{
			pos = command1.find(' ');   // find the first arg
			if(pos ==string::npos)	   // last arg?
			{
				ss.str(command1);   //string to stream from
				ss>>argv2[c];	   
                                //stream to a temp array to point at

				argv[c]=argv2[c];   // point at array
			}
			else
			{
				command2=command1.substr(0,pos);
				ss.str(command2);	// see above
				ss>>argv2[c];
				argv[c]=argv2[c];
				command1.erase(0,pos+1);	
                                //remove stored command from copy of command

				c++;	// inc counter
				ss<<' ';
			}
		};
	}while(pos != string::npos);
	pid_t child=fork();		// mk child
	if(child==0)
	{
		if(execvp(arg1.c_str(), argv)==-1)
                //run prog error out and kill if errors
		{
			perror("execvp");
			pid_t curr_pid=getpid();   // get child pid
			kill(curr_pid, 9);	//kill with signal 9
		}
	}
	else
	{
		wait(NULL);	// wait till child completes	
	};

  }

The same process can be used to start a process in the background, however the parent need not wait for the child to finish. whether or not the program should be started in the background can be determined with an if statement checking if the last character in the command is an ampersand.

Now for the final function of our list, the piping, to do this we will need to utilize two new functions, dup2() and pipe(). dup2() is used to copy a file descriptor, and pipe creates a new pair of file descripters, which allow the parent and child to communicate(0 is for writing, 1 for reading). For dup2() 0 is stdin, 1 is stdout, and 2 is stderr. Therefore the first thing we need to do is create our pipe.

	int pipe_array[2];
	pipe(pipe_array);

Now to add some functionality to the code. It is essentially the same as our earlier function but we add this if statement after we get the command 2 substring

  if(command2 =="|")
  {
	pos = command1.find(' ');		// find the first arg
	command1.erase(0,pos+1);
	pid_1=fork();
	if(pid_1==0)
	{
	// child
	// redirect the stdout
		if(dup2(pipe_array[1],1)==-1) //copy stdout to pipe_array write
		{
			perror("dup2");
		}
		if(execvp(arg1.c_str(), argv)==-1)//run prog
		{
			perror("execvp");
			pid_t curr_pid=getpid();   // get child pid
			kill(curr_pid, 9);	//kill with signal 9
		}	
	}
	else	//parent
	{	
		wait(NULL);
		close(pipe_array[1]);
		prog_with_pipe(command1); // start second process
		command1.clear();
	};
  }

and don't forget the file descriptor copy in the second parent added after our while loop.

  if(string::npos==command.find("|")) //if no more pipes execute final prog
  {
	pid_t child=fork();		// mk child
	if(child==0)
	{
		if(dup2(pipe_array[0],0)==-1) // copy stdin to pipe read
		{
			perror("dup2");
		}
		if(execvp(arg1.c_str(), argv)==-1)	
                //run prog error out and kill if errors
		{
			perror("execvp");
			pid_t curr_pid=getpid();   // get child pid
			kill(curr_pid, 9);	//kill with signal 9
		}
	}
  else
  {
		close(pipe_array[0]);	
		wait(NULL);		// wait till child completes	
  };

Since we are only going for basic piping here this code will only work with one pipe, However it is quite possible to have multiple pipes, again this is an exercise I leave to the reader, Just be sure to remember that pipes are one way, and to mind your openings and closings.

We have now created a program, which has built in functions, can spawn processes, and has single command piping. A very basic shell, but usable. I invite the reader to add their own functions to make it usable for them. Should you wish your new program can be set as you shell using the chsh command. So play around with it, maybe you'll learn something new.

-Nick S. aka Nickisgod1


Analog5 Front Page