You’re only as young as you feel. Until something reminds you of just how old you are. Or, in this case, how long you’ve been at something, which by extension reminds you of how you old you are.

I’ve been programming in C++ for almost 30 years and that’s both awesome and weird. And with 30 years of experience it’s easy to forget how your code looked back in the day. Earlier today, while searching for something, I stumbled upon a bit of homework from my time as an undergrad student at UNLV, and after reviewing it, I found myself equal parts surprised, embarassed and amused.

And so, I figured it would make for a fun post.

So the homework assignment was to write a simple shell in C, which would allow the user to enter commands, which it would then fork a process and execute, while maintaining a ‘history’ of the commands previously entered in the shell by the user.

Doing the bare minimum was never my style. I had to go all out. My shell had to print stuff out in color. It had to support pipes and background processes. And, of course, proper reaping of jobs. I’m sure the TA that had to grade my code was not amused. I don’t remember his name, and it’s unlikely he’ll ever read this, but yeah… sorry about that 😅


// MIT License
//
// Copyright (c) 1998 Nikolaos D. Bougalis <nikb@bougalis.net>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <termios.h>
#include <term.h>
#include <assert.h>
#include <string.h>
#include <strings.h>
#include <pwd.h>
#include <ctype.h>

#define COLOR_BOLD 1
#define COLOR_LITE 2

#define COLOR_BLACK   30
#define COLOR_RED     31
#define COLOR_GREEN   32
#define COLOR_YELLOW  33
#define COLOR_BLUE    34
#define COLOR_MAGENTA 35
#define COLOR_CYAN    36
#define COLOR_GREY    37

#define KEY_ARROW       0x5B
#define KEY_ARROW_UP    0x41
#define KEY_ARROW_DOWN  0x42
#define KEY_ARROW_LEFT  0x44
#define KEY_ARROW_RIGHT 0x43

// We store the username of the user we are running under so that
// we can display it when necessary:
char *shell_user = NULL;

// We store the "current working directory" so that we can display
// it when we have to print a prompt.
char *shell_cwd = NULL;

// We store the user's home directory so that we can reference it
// when necessary.
char *shell_home = NULL;

// A couple of special-purpose buffers:
char shell_ampsnd_str[2] = { '&', 0 };
char shell_pipeln_str[2] = { '|', 0 };

// We keep track of the last SHELL_HIST_MAX input buffers that the
// user has specified using a circular array of character pointers. 
#define SHELL_HIST_MAX 12

char *shell_hist_cmd[SHELL_HIST_MAX];
int shell_hist_idx = 0;

// This variables keep track of how many times that "up" and
// "down" keys have been pressed respectively while the user navigates
// the command history. They are automatically reset every time the
// shell prepares to grab a new line.
int shell_hist_updn = 0;

// We keep track of SHELL_BGPROC_MAX processes using this structure:
#define SHELL_BGPROC_MAX 12

struct BGPROC_TRACK
{
	pid_t pid;
	char *cmd;
};

struct BGPROC_TRACK shell_bgproc[SHELL_BGPROC_MAX];

void internal_shell_abort(const char *s, const char *f, int l)
{	// Print the current error, along with where it occurred and then exit
	printf("\x1B[%d;%dmA fatal error has occurred!\x1B[0m\n", COLOR_LITE, COLOR_RED);
	
	if((s != NULL) && (*s != 0))
		printf("\tError Location: %s:%d (%s)\n", f, l, s);
	else
		printf("\tError Location: %s:%d\n", f, l);
		
	printf("\tError Message: %s\n", strerror(errno));
	
	printf("\x1B[%d;%dmThe shell will now exit. Goodbye!\x1B[0m\n", COLOR_LITE, COLOR_RED);
		
	// This isn't strictly necessary; stdout is line-buffered, so putting a newline
	// automatically flushes the output. But be extra conservative and make sure
	// we flush.
	fflush(stdout);
	
	_exit(-1);
}

// This macro is what we call to abort the shell. It automatically passes the
// correct filename and line number at which the error occurred.
#define shell_abort(s) internal_shell_abort(s, __FILE__, __LINE__)

void internal_shell_error(const char *s, int err, const char *f, int l)
{	// Print the string in cmd
	printf("\x1B[%d;%dmAn error has occurred!\x1B[0m\n", COLOR_LITE, COLOR_RED);
	
	if((s != NULL) && (*s != 0))
		printf("\tError Location: %s:%d (%s)\n", f, l, s);
	else
		printf("\tError Location: %s:%d\n", f, l);
		
	printf("\tError Message: (%d): %s\n", errno, strerror(errno));	
	
	// This isn't strictly necessary; stdout is line-buffered, so putting a newline
	// automatically flushes the output. But be extra conservative and make sure
	// we flush.	
	fflush(stdout);
}

// This macro is what we call to report errors from the shell. It automatically
// passes the correct filename and line number at which the error occurred.
#define shell_error(s, n) internal_shell_error(s, n, __FILE__, __LINE__)

void shell_syntax_error(const char *s)
{	// Print the string in cmd
	printf("\x1B[%d;%dmSyntax error\x1B[0m", COLOR_LITE, COLOR_RED);
	
	if((s != NULL) && (*s != 0))
		printf(": %s", s);
	
	printf("\n");
	
	// This isn't strictly necessary; stdout is line-buffered, so putting a newline
	// automatically flushes the output. But be extra conservative and make sure
	// we flush.	
	fflush(stdout);
}

void print_prompt()
{	
	const char *cwd = shell_cwd;
	int color = COLOR_GREEN;
	
	if(cwd == NULL)
	{	// If the directory isn't known, highlight it in color
		cwd = "[???] > ";
		color = COLOR_RED;
	}

	// We print a fancy colored prompt using ANSI
	// escape sequences.	
	printf("\x1B[%d;%dm%s\x1B[0m > ", COLOR_LITE, color, cwd);
	
	fflush(stdout);
}

char *shell_getcwd()
{	// Get the current working directory from the system. 
	// The pointer returned must be freed.
	char *cwd = NULL;
	size_t len = 0;
	
	do
	{			
		len += 512;
		
		cwd = (char *)malloc(len + 1);
		
		if(cwd == NULL)
			shell_abort("shell_getcwd");
		
		if(getcwd(cwd, len) == NULL)
		{	// We couldn't get the current directory. First, free
			// the memory buffer that we have allocated, then check
			// to see what the problem was.
			
			if(cwd != NULL)
				free(cwd);
				
			cwd = NULL;
		
			if(errno == ENOENT)
			{	// The current directory does not exist. Instead of 
				// panicking, let's simply switch to the user's home
				// directory and get that instead.
				
				if((shell_home != NULL) && (chdir(shell_home) != -1))
				{	// Success! We've switched to the home directory, so
					// we can strdup the shell_home pointer and return it:
					
					cwd = strdup(shell_home);
					
					if(cwd == NULL)
						shell_abort("getcwd");
						
					return cwd;	
				}
			}				
			
			if(errno != ERANGE) // This is an error we don't know how to deal with.
				shell_abort("getcwd");	
		}		
	} while(cwd == NULL);

	return cwd;	
}

int parse_command(char *linebuf, char ***argv_ptr, int *amp, int *pp)
{	// We parse the command in 'linebuf' into an array of tokens
	// that are separated by spaces. We mangle the 'linebuf'
	// contents in the process.
	char empty[1];
	
	if((amp == NULL) || (pp == NULL))
		shell_abort("parse_command");
		
	if(linebuf == NULL)
		linebuf = empty;
		
	// We skip multiple whitespaces at the beginning of an input
	// string since they are irrelevant.	
	while(*linebuf == ' ')
		linebuf++;
	
	// We allocate an array of character pointers into which
	// we store the tokenized string. We expand the array as
	// necessary, when we run out of space.	
	int argc = 0, alloc_argc = 0;		
	char **argv = (char **)malloc(sizeof(char *));
	
	*amp = 0;
	*pp = 0;
	
	while(*linebuf != 0)
	{
		if(argc == alloc_argc)
		{	// If we don't have enough memory to store the new
			// token, allocate some extra now.
			alloc_argc += 8;
			
			// 'argv' will be NULL during the first call, which is
			// fine, because then realloc behaves like 'malloc'
			// 
			// We add one extra item to allow space for the terminating
			// NULL pointer we will add.
			argv = (char **)realloc(argv, sizeof(char *) * (alloc_argc + 1));
			
			if(argv == NULL)
				shell_abort("parse_command");
		}
		
		if(*linebuf == '&')
		{ // An ampersand character! Track it and put it in a token by itself:
			argv[argc++] = shell_ampsnd_str;
			*linebuf++ = 0;
			*amp = 1 + *amp;
		}
		else if(*linebuf == '|')
		{ // A pipeline character! Track it and put it in a token by itself:
			argv[argc++] = shell_pipeln_str;
			*linebuf++ = 0;			
			*pp = 1 + *pp;
		}
		else
		{
			// Now, store the pointer into the current token into our
			// token array, and increment the array pointer.
			argv[argc++] = linebuf;
					
			// Now advance until we get a space, ampersand, pipeline
			// or NULL character.
			while((*linebuf != 0) && (*linebuf != ' ') && 
			      (*linebuf != '|') && (*linebuf != '&'))
				*linebuf++;
		}
		
		// If it's a space, we replace it with a NULL character
		// which effectively terminates the current token. We
		// increment our pointer to get to the next non-space
		// character, and then continue with the main loop.
		while(*linebuf == ' ')
			*linebuf++ = 0;
	}
	
	// We are guaranteed to have enough space to put the terminating
	// NULL pointer, so this is always safe:
	argv[argc] = NULL;
	
	// Now, we want to give our caller back a pointer to the tokenized
	// string array. 				
	*argv_ptr = argv;
	
	// And also how many arguments can found.
	return argc;
}

int check_builtin(int argc, char **argv)
{	// Checks whether a particular command is internally handled by our
	// shell. If so, executes the appropriate code and returns a status
	// code that is greater than or equal to 0.
	// 
	// If we don't handle the command, we simply return -1.
	
	if((strcasecmp(argv[0], "exit") == 0) || 
	   (strcasecmp(argv[0], "quit") == 0))
	{	// First, check if the user typed 'exit' or 'quit' which
	 	// indicate that we are done.
		printf("Goodbye!\n");
					
		// Returning 0 to our caller will exit the shell gracefully
		return 0;
	}
				
	// Now, check what the user wants us to do. First, determine if
	// this is one of the built-in commands that we support.
	if((strcasecmp(argv[0], "hist") == 0) || 
	   (strcasecmp(argv[0], "history") == 0))
	{
		int hasone = 0, i;
		
		for(i = 0; i < SHELL_HIST_MAX - 1; i++)
		{
			int idx = (shell_hist_idx + i) % SHELL_HIST_MAX;

			if(idx < 0)
				idx += SHELL_HIST_MAX;
		
			if(shell_hist_cmd[idx] != NULL)
			{
				if(hasone++ == 0)
					printf("Command history:\n");
				
				printf("    \x1B[%d;%dm%s\x1B[0m\n", COLOR_LITE, COLOR_BLUE, shell_hist_cmd[idx]);
			}						
		}
		
		if(hasone == 0)
			printf("The command history is empty.\n");
			
		return 1;
	}
	
	if((strcasecmp(argv[0], "cd") == 0) || 
	   (strcasecmp(argv[0], "chdir") == 0))
	{
		if(argc == 1)
		{	// No argument specified; print the current directoy (if we can)
			if(shell_cwd == NULL)
				printf("The current directory is \x1B[%d;%dmunknown\x1B[0m!\n", 
					COLOR_LITE, COLOR_RED);
			else
				printf("The current directory is \"\x1B[%d;%dm%s\x1B[0m\"\n",
					COLOR_LITE, COLOR_GREEN, shell_cwd);
				
			return 1;
		}
		
		if(argc != 2)
		{	// We don't really support paths with spaces for our
			// simple shell.
			printf("\x1B[%d;%dm%s\x1B[0m: Paths with spaces are not supported\n", 
				COLOR_LITE, COLOR_RED, argv[0]);
			return 1;
		}
		
		const char *cd = argv[1];
		
		if(strcmp(cd, "~") == 0) // User convenience. ~ maps to home directory
			cd = shell_home;

		if(chdir(cd) == -1)
		{	// An error occurred. Print an error message, and if the error
			// was fatal, abort.			
			if((errno == ENOENT) || (errno == ENOTDIR) || (errno == EACCES))
			{
				printf("\x1B[%d;%dm%s\x1B[0m: %s\n", COLOR_LITE, COLOR_RED, 
					argv[0], strerror(errno));
				return 1;
			}	
			
			shell_abort("user_chdir");
			
			// We never get here, since shell_abort exits the process
			return 1;
		}
	
		if(shell_cwd != NULL)
			free(shell_cwd);
			
		shell_cwd = shell_getcwd();		
		
		if(shell_cwd == NULL)
			shell_abort("user_chdir_getcwd");
			
		// We want to continue doing things
		return 1;
	}

	if(strcasecmp(argv[0], "bg") == 0)
	{
		int hasone = 0, i;
		
		for(i = 0; i < SHELL_BGPROC_MAX; i++)
		{
			if(shell_bgproc[i].pid == -1)
				continue;
				
			if(hasone++ == 0)
				printf("Tracked background processes:\n");
				
			printf("    [ \x1B[%d;%dm%6d\x1B[0m ] %s\n", 
				COLOR_LITE, COLOR_BLUE, shell_bgproc[i].pid, shell_bgproc[i].cmd);
		}
		
		if(hasone == 0)
			printf("No tracked background processes are running.\n");
			
		return 1;
	}
		
	// We don't know how to handle this command. Try to see if there
	// is an external program.
	return -1;
}

int launch_process(char *file, char **argv, int bg)
{
	pid_t kid = fork();
	
	if(kid == -1)
	{ 	// Ouch, an error occurred. Print a somewhat informative
		// error message and return.
		shell_error(file, errno);		
		return 1;
	}
	
	// OK, we successfully forked. 
	if(kid == 0)
	{	// The child gets to exec the new process now. Technically,
		// execvp will only return to us if there is an error, so
		// when that happens, we will simply print our an error
		// message and exit.
		
		if(execvp(file, argv) == -1)
			_exit(errno); // Ack! An error happened; pass the code to our parent!
					
		// We will never really get here. 	
		_exit(0);		
	}

	if(bg == 0)
	{	// We are the parent and we want to run this in the foreground,
		// so we must wait for our child.
		int kid_status = 0;
		
		if(waitpid(kid, &kid_status, 0) == -1)
			shell_error(file, errno);
	
		if(WIFEXITED(kid_status))
		{
			int err = WEXITSTATUS(kid_status);
			
			if(err != 0)
			{
				if((err == ENOENT) || (err == ENOEXEC))
					printf("\x1B[%d;%dm%s\x1B[0m: %s\n", COLOR_LITE, COLOR_RED, 
						file, strerror(err));
				else									
					shell_error(file, errno); 
			}
		}
		
		return 1;
	}
	
	// Let's add this process in the array that keeps track of background processes
	// if there's space:
	int bgidx = 0;
	
	while(bgidx != SHELL_BGPROC_MAX)
	{
		if(shell_bgproc[bgidx].pid != -1)
		{
			bgidx++;
			continue;
		}
		
		shell_bgproc[bgidx].pid = kid;
		shell_bgproc[bgidx].cmd = strdup(file);
		
		break;
	}
	
	if(bgidx == SHELL_BGPROC_MAX)
	{	// There was no space left to track the execution of this process in the 
		// background process tracking array. Warn the user and continue.
		printf("\x1B[%d;%dm%s\x1B[0m: Unable to track background execution (out of space)\n", 
			COLOR_LITE, COLOR_RED, file, strerror(errno));
	}
		
	return 1;
}

int process_arrow_keys(char **linebuf)
{
	if(linebuf == NULL)
	{
		shell_error("invalid line buffer", EINVAL);
		return 0;
	}
	
	char extra[2] = { 0, 0 };
			
	if((read(0, extra, 2) == 2) && (extra[0] == KEY_ARROW))
	{	// Cool, this is a code we're interested in.
		if(extra[1] == KEY_ARROW_UP)
		{	
			*linebuf = NULL;
	
			if(shell_hist_updn == SHELL_HIST_MAX)
			{	// We're at the top -- sound the bell
				printf("\a");
				fflush(stdout);
			}
					
			if(shell_hist_updn != SHELL_HIST_MAX)
			{ 	// We still have 'space' in our circular buffer to 
				// go up one more command in the history.
				shell_hist_updn++;
				
				int idx = (shell_hist_idx - shell_hist_updn) % SHELL_HIST_MAX;

				if(idx < 0)
					idx += SHELL_HIST_MAX;
			
				*linebuf = shell_hist_cmd[idx];
			}
			
			// We aren't done with the processing of the current command.
			return 0;
		}

		if(extra[1] == KEY_ARROW_DOWN)
		{
			*linebuf = NULL;
			
			if(shell_hist_updn == 1) 
			{ 	// We're at the bottom -- sound the bell
				printf("\a");
				fflush(stdout);
			}
						
			if(shell_hist_updn > 1)
			{ 	// We still have 'space' to go down one more command in
				// our circular buffer history.
				shell_hist_updn--;
				
				int idx = (shell_hist_idx - shell_hist_updn) % SHELL_HIST_MAX;
				
				if(idx < 0)
					idx += SHELL_HIST_MAX;
				
				*linebuf = shell_hist_cmd[idx];					
			}
			
			// We aren't done with the processing of the current command.
			return 0;
		}		
	}
	
	return 0;
}

int launch_process_redirect(const char *file, char **argv, int oldfd, int newfd)
{
	pid_t kid = fork();
	
	if(kid == -1)
	{ 	// Ouch, an error occurred. Print a somewhat informative
		// error message and return.
		shell_error(file, errno);		
		return -1;
	}
	
	// OK, we successfully forked. 
	if(kid == 0)
	{	// Cool, this is the child; first, redirect the appropriate
		// file descriptor to the new file descriptor.
		
		if((oldfd != -1) && (newfd != -1))
		{ // We need to remap oldfd so that it points to newfd:
			if((close(oldfd) != -1) && (dup(newfd) != oldfd))
				_exit(EBADFD); // Ack!
		}						
		
		// The child gets to exec the new process now. Technically,
		// execvp will only return to us if there is an error, so
		// when that happens, we will simply print our an error
		// message and exit.				
		if(execvp(file, argv) == -1)
			_exit(errno);
			
		// We will never really get here. 	
		_exit(0);		
	}

	// We are the parent and we want to run this in the foreground,
	// so we must wait for our child.
	int kid_status = 0;

	if(waitpid(kid, &kid_status, 0) == -1)
		shell_error(file, errno);

	// We need to close the fd that we redireted to avoid leaking
	// file handles.
	close(newfd);
		
	// And print an appropriate error message, if necessary.		
	if(WIFEXITED(kid_status))
	{
		int err = WEXITSTATUS(kid_status);
		
		if(err != 0)
		{
			if((err == ENOENT) || (err == ENOEXEC))
				printf("\x1B[%d;%dm%s\x1B[0m: %s\n", COLOR_LITE, COLOR_RED, 
					file, strerror(err));
			else									
				shell_error(file, errno); 
		}
	}
	
	return 1;
}

int handle_command(int argc, char **argv, int amp, int ppln)
{
	if(argc == 0)
		return 1;
		
	if((amp != 0) && (amp != 1))
	{	// Exactly 0 or 1 ampersands are allowed by our shell.
		shell_syntax_error("cannot have more than one & character");
		return 1;				
	}

	if((ppln != 0) && (ppln != 1))
	{	// Exactly 0 or 1 pipes are allowed by our shell.
		shell_syntax_error("cannot have more than one | character");
		return 1;
	}			
			
	if((amp != 0) && (amp + ppln != 1))
	{	// You cannot combine background processes with pipes in our shell
		shell_syntax_error("cannot combine background processes with pipelines");
		return 1;
	}
			
	int ret = -1;
				
	// Let's see if it's a builtin command that we understand, or if it's
	// one that we need to launch.
	if((amp == 0) && (ppln == 0))			
		ret = check_builtin(argc, argv);
							
	if(ret == -1) 
	{	// Well, we need a bit of extra work. This might be a simple
		// command that we need to fork and execute, or it might be
		// a pipelined set of commands. Either way, handle it:			

		if(ppln == 1)
		{	// OK, this is a pipeline. We want to parse out the two
			// sets of commands that are in our input buffer. Remember
			// that they are separated by a | character, so iterate
			// and find that character, replacing it by a NULL.
			char **argv1 = argv;
			char **argv2 = argv;
			
			while((argv2 != NULL) && (strcmp(*argv2, "|") != 0))
				argv2++;
				
			if(argv2 == NULL)			
			{
				shell_syntax_error("unable to parse pipeline");
				return 1;
			}
			
			*argv2++ = NULL;

			if(*argv2 == NULL)
			{
				shell_syntax_error("incomplete pipeline (the | character must not be at the end of the command)");
				return 1;
			}
			
			// Good, now, argv1 points to the command and arguments of the first
			// part of the pipeline and argv2 points to the command and arguments
			// of the second part of the pipeline. We continue by setting up the
			// pipes we will be using:
			int pipes[2];
			
			if(pipe(pipes) == -1)
			{
				shell_error("pipe", errno);
				return 1;
			}

			// Launch the first process in the pipeline, wait for it to finish
			// and if it does so successfully, launch the second process.			
			if(launch_process_redirect(argv1[0], argv1, 1, pipes[1]) != -1)
				launch_process_redirect(argv2[0], argv2, 0, pipes[0]);
			
			return 1;
		}
		
		if(amp == 1)
		{
			if(strcmp(argv[argc - 1], shell_ampsnd_str) != 0)
			{	// The ampersand that we use to put a job in the background
				// must be at the end. 	
				shell_syntax_error("the & character must be at the end of the command");
				return 1;	
			}

			// Trim the ampersand off the command line we're about to execute
			argc--;
			argv[argc] = NULL;
		}
					
		ret = launch_process(argv[0], argv, amp);
	}
	
	return ret;										
}

int process_line()
{	// We print a prompt, accept the user input, process it and
	// return to our caller. We return '1' if we need to continue
	// running, and 0 otherwise.
	
	// First, make sure that our up/down handlers know what they
	// need to do and from where they need to start.
	
	shell_hist_updn = 0;
	
	print_prompt();
		
	int linelen = 128, lineidx = 0, tmp;
	char *linebuf = (char *)malloc(linelen), key;
	
	while(read(0, &key, 1) == 1)
	{
		if(key == 0x1B)
		{	// This is the beginning of a three-letter combo 
			// that we may be interested in... Let's see:
			char *hist = NULL;
			
			if(process_arrow_keys(&hist))
				return 1;
			
			if(hist != NULL)
			{ 	// We need to replace the current input buffer
				// with whatever the contents in hist are.
				
				// First, we need to clear out what's already there
				// by backing up, replacing it with spaces and then
				// backing up again:
				for(tmp = 0; tmp < lineidx; tmp++)
					printf("\b");
				
				for(tmp = 0; tmp < lineidx; tmp++)
					printf(" ");
				
				for(tmp = 0; tmp < lineidx; tmp++)
					printf("\b");
				
				int histlen = (int)strlen(hist);
				
				if(histlen + 2 >= linelen)
				{	// We need to allocate a new buffer
					free(linebuf);
					
					linelen = histlen + 64;
					
					linebuf	= (char *)malloc(linelen);
					
					if(linebuf == NULL)
						shell_abort("malloc-linebuf-hist");
				}
	
								
				// And now, copy the new command into our line buffer
				lineidx = histlen;
				
				strcpy(linebuf, hist);					
				
				printf("%s", linebuf);
				
				fflush(stdout);
			}
		
			continue;
		}
		
		if(key == 0x7F)
		{	// We have to handle a backspace character				
			if(lineidx == 0)
			{	// We can't backspace if we're at the beginning of the
				// line. So just sound the bell and do nothing.
				printf("\a");
				fflush(stdout);
				continue;
			}
				
			// Decrement our line index, so that on the next keystroke we 
			// effectively erase the last character from our input buffer.
			lineidx--;
				
			// And delete it from the screen. First back the cursor up one
			// position, print a space, and then back the cursor up one
			// position again.
			printf("\b \b");				
			fflush(stdout);
				
			continue;
		}
			
		if(isprint(key))
		{	// Ahh, a printable character. First, we want to add it to our
			// input buffer (enlarging it if necessary) and then we want
			// to print it to the screen. Note, that we always leave one
			// free byte at the end of the buffer, so that the end-of-line
			// handler has a guaranteed spot to null-terminate the string.
		                                
			if(lineidx + 2 == linelen)
				linelen += 128;
			
			linebuf = (char *)realloc(linebuf, linelen);
				
			if(linebuf == NULL)
				shell_abort("realloc-linebuf");
				
			printf("%c", key);
			fflush(stdout);
								
			linebuf[lineidx++] = key;
			continue;	
		}
			
		if(key == '\n')
		{	// Cool, the user pressed enter.
			// First, NULL-terminate the input string.
			linebuf[lineidx] = 0;

			// We want to copy our current command into the history buffer
			// so save it now, before we hack it up with our parser:
					
			// First, if we were already storing a command there, release
			// it, freeing the associated memory buffer.
			if(shell_hist_cmd[shell_hist_idx] != NULL)
				free(shell_hist_cmd[shell_hist_idx]);
				
			// Now, store a duplicate of the new command.
			shell_hist_cmd[shell_hist_idx] = strdup(linebuf);
			
			// And manage the list index so it's ready for the next
			// time around:
			shell_hist_idx = (shell_hist_idx + 1) % SHELL_HIST_MAX;
				
			// We want to output a newline string, so that any output in
			// response to the user's command starts on a newline.
			printf("\n");
						
			// We want to tokenize the input string into an array. 
			int amp, ppln, i, ret = 1;
			char **argv = NULL;
						
			int argc = parse_command(linebuf, &argv, &amp, &ppln);
			
			if(argc != 0)
				ret = handle_command(argc, argv, amp, ppln);
			
			if(argv != NULL)
				free(argv);
					
			// We return to our caller with an appropriate error code:
			return ret;
		}		
	}
	
	// The 'read' from the terminal couldn't complete.
	shell_abort("term-read");
	
	// Never really called.
	return 0;
}

void grim_reaper_info(pid_t pid, const char *cmd, int status)
{	// Print a short information line about the process that
	// just exited.
	const char *stat = "Done";
	int code = 0;
				
	if(WIFEXITED(status))
	{
		stat = "Exited";
		code = WEXITSTATUS(status);	
	}
	else if(WIFSIGNALED(status))
	{	
		stat = "Killed";
		code = WTERMSIG(status);
	}
	
	printf("[ \x1B[%d;%dm%6d\x1B[0m ]\t%7s (%d)", 
		COLOR_LITE, COLOR_BLUE, pid, stat, code);
	
	if(cmd != NULL)
		printf("\t    %s", cmd);

	printf("\n");
}

void grim_reaper()
{	// Reaps any childer that were executing in the background and have
	// finished, printing relevant status information:	
	pid_t ret;
	
	do
	{
		int kid_status = 0;
		
		ret = waitpid(-1, &kid_status, WNOHANG);
		
		// If we get an ECHILD error, it means there's no child processes
		// at all. Which is fine -- we can ignore that error and simply
		// exit from the loop.
		if((ret == -1)  && (errno == ECHILD))		
			ret = 0;
			
		if(ret == -1)
		{ 	// An error has occurred:
			shell_error("wait-any-pid", errno);
			return;
		}
		
		if(ret != 0)
		{	// Ah, a child of ours was done. Let's see which:
			int bgidx = 0;
			
			while(bgidx != SHELL_BGPROC_MAX)
			{
				if(shell_bgproc[bgidx].pid != ret)
				{
					bgidx++;
					continue;
				}
					
				// Figure out the status as best as we can:
				grim_reaper_info(shell_bgproc[bgidx].pid, shell_bgproc[bgidx].cmd, kid_status);
				
				// And free this slot from the table	
				free(shell_bgproc[bgidx].cmd);
			
				shell_bgproc[bgidx].cmd = NULL;
				shell_bgproc[bgidx].pid = -1;
					
				break;
			}
			
			if(bgidx == SHELL_BGPROC_MAX) // Ack, we couldn't find this child! Just print its PID 
				grim_reaper_info(ret, NULL, kid_status);
		}	  
	} while(ret != 0);
}

int interactive_shell()
{
	// We need to get some information about the user we are running
	// as: namely, the username and their home directory. 
	struct passwd *passwd = getpwuid(geteuid());
	int i;
		
	if(passwd == NULL)
		shell_abort("getpwuid");
		
	// First, save the username:
	if((passwd->pw_name == NULL) || (passwd->pw_name[0] == 0))
		shell_abort("getpwuid:nousername");
		
	shell_user = strdup(passwd->pw_name);
	
	if(shell_user == NULL)
		shell_abort("strdup:pw_name");
		
	// Now for the home directory, if one is set
	if((passwd->pw_dir != NULL) && (passwd->pw_dir[0] != 0))
	{
		shell_home = strdup(passwd->pw_dir);	
		
		if(shell_home == NULL)
			shell_abort("strdup:pw_dir");
	}
	
	// If we haven't gotten the directory yet, try to 
	// expand the HOME environment variable:
	if((shell_home == NULL) || (shell_home[0] == 0))
	{
		char *home = getenv("HOME");
	
		if(home != NULL)
			shell_home = strdup(home);
	}
	
	if((shell_home == NULL)	|| (shell_home[0] == 0))
		shell_abort("get-homedir");
		
	// Now, get the current working directory from the system
	// and store it. We use it to display our prompt.
	shell_cwd = shell_getcwd();
	
	if(shell_cwd == NULL)
		shell_abort("get-cwd");

	// Set up our circular buffer:
	for(i = 0; i < SHELL_HIST_MAX; i++)
		shell_hist_cmd[i] = NULL;
	
	// And setup the background process tracking array:
	for(i = 0; i < SHELL_BGPROC_MAX; i++)
	{
		shell_bgproc[i].pid = -1;
		shell_bgproc[i].cmd = NULL;
	}
		
	// Print a banner.
	puts("\x1B[2;32mWelcome to the CS370 shell prompt.\x1B[0m");
	puts("\tWritten by Nikolaos D. Bougalis <old email>\n");
	
	puts("\x1B[2;32mYou can use all the normal UNIX commands that are");
	puts("normally available to you.\n\n\x1B[0m");

	// We loop, accepting input, for as long as process_line returns
	// non-zero. Between processing, wait for any of our background
	// children and reap them as soon as they're done.
	while(process_line() != 0)
		grim_reaper();
				
	return 0;
}

int main(int argc, char **argv)
{
	// This is the first invocation of the shell so what
	// we want to do is set some terminal attributes and
	// then launch ourselves again to interact with the
	// user. We do this so that if the shell crashes we
	// still get a chance to reset the terminal modes.
	
	struct termios termcfg_old, termcfg_new;
	  	
	if(tcgetattr(0, &termcfg_old) == -1)
	{
		shell_error("tcgetattr", errno);
		return -1;
	}		
  	
	// Make a copy of the current configuration, so that we 
	// modify that copy as necessary.
	memcpy(&termcfg_new, &termcfg_old, sizeof(struct termios));
	  	
	termcfg_new.c_lflag &= ~(ICANON | ECHO);
	termcfg_new.c_cc[VMIN] = 10;
	termcfg_new.c_cc[VTIME] = 2;
	  	
	if(tcsetattr(0, TCSANOW, &termcfg_new) == -1)
	{
		shell_error("tcsetattr", errno);
		return -1;
	}
	  	
	pid_t kid = fork();

	if(kid == -1)
	{
		shell_error("fork-interactive", errno);
		return -1;
	}
		
	// If kid is 0, this means that we are executing in the child process
	// which will serve as the actual shell. We isolate all the shell code
	// in a routine specifically designed for this purpose. When it returns
	// it means the user chose to exit our shell, so we just exit, terminating
	// the process and returning control to the parent, which is waiting.		
	if(kid == 0)
		return interactive_shell();

	// The fork was a success, and we just have to wait for the interactive
	// component of the shell to exit now:
	int kid_status = 0;
	
	if(waitpid(kid, &kid_status, 0) == -1)
		shell_error("wait-interactive", errno);
		
	// Now, reset the terminal mode -- we need to do this whether
	// the child launched correctly or not. Otherwise, we'll leave
	// the user with a botched terminal.
		
	if(tcsetattr(0, TCSANOW, &termcfg_old) == -1)
	{
		shell_error("tcsetattr", errno);
		return -1;
	}
	
	return (kid != -1) ? 0 : 1;
}

So there you have it. In all its poorly-formatted, excessively documented glory. Overall, I think I did a good job, but it’s certainly the work of someone without a lot of experience developing code professionally.

Oh, and if your school project requires you to write a shell, feel free to study the code. But don’t copy-paste it: you’ll only be doing yourself a disservice.