/*
	shell.c, a very simple shell program for the CS420 shell lab.
	
	Charlie Peck, multiple revisions between 2003 and 2009.  Thanks to Toby McNulty for providing 
	a nice starting point.
	
	Build with '$ gcc -lreadline shell.c -o shell'
	
	Improvements which could be made to this code:
		a) Check return values from all system calls using errno
		b) It's optimized to be concise, it could be less obtuse in a couple of places 
		c) ~ support, requires detecting it and re-writing the command before searching PATH
		d) Support for redirection and backgrounding
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/param.h> // home of MAXPATHLEN
#include <stdlib.h>
#include <readline/readline.h>

const int MAXCMDLEN = 1024;
const int MAXARGS = 64;
const int MAXPROMPTLEN = 512;
const int DEBUG = 0;

/* A static variable for holding the line. */
static char *line_read = (char *)NULL;

/* Read a string, and return a pointer to it.  Returns NULL on EOF. */
char *rl_gets(char *prompt) {
	/* If the buffer has already been used free it. */
	if (line_read) {
		free(line_read);
		line_read = (char *)NULL;
	}

	/* Get a line from the user. */
	line_read = readline(prompt);

	/* If the line has any text in it save it on the history. */
	if (line_read && *line_read) 
		add_history(line_read);

	return(line_read);
}

/* Find the executable in PATH, or not as the case may be. */ 
void findexec(char *name, char *outpath) {
	char *paths = (char*)getenv("PATH"), *onepath;
	char *temppath = malloc(MAXPATHLEN);
	char sep[] = ":";
	
	/* Work with a copy of paths so we don't bork the original with strtok. */
	char *mypaths = malloc(strlen(paths) + 1);
	strcpy(mypaths, paths);
	
	outpath[0] = '\0';
	
	if (name[0] == '/' || name[0] == '.') {
		/* absolute or relative path */
		strcpy(outpath, name);

	} else {
		/* system path; iterate through mypaths checking for an executable */
		for (onepath = strtok(mypaths, sep); onepath; onepath = strtok(NULL, sep)) {
			snprintf(temppath, MAXPATHLEN, "%s/%s", onepath, name);

			if (access(temppath, X_OK) == 0) {
				/* it's executeable, take it */ 
				strcpy(outpath, temppath);
			}
		}
	}
	
	free(mypaths);
	free(temppath);
	return; 
}

/* Prompt the user and collect their command input. */ 
char *getcmd() {
	char cwd[MAXPATHLEN], hostname[255], prompt[MAXPROMPTLEN];
	char *dot, *rline;
	
	/* build the prompt string */
	getcwd(cwd, MAXPATHLEN);
	gethostname(hostname, 255);
	
	/* shorten the hostname for the prompt string */
	dot = strchr(hostname, '.');
	if (dot) 
		*dot = '\0';

	snprintf(prompt, MAXPROMPTLEN, "%s@%s:%s $ ", getlogin(), hostname, cwd);
	
	/* use readline to fetch input */
	rline = rl_gets(prompt);
	
	if (DEBUG)
		fprintf(stderr, " ** got input: '%s' (len=%i)\n", rline, (int)strlen(rline));
	
	return(rline);
}

void forkandexec(char *path, char *args) {
	char *arg, *myargs, *execnm = strrchr(path, '/') + 1;
	char *argv[MAXARGS];
	char sep[] = " ";
	int status, pid, argn = 1;
	
	if (DEBUG) 
		fprintf(stderr, " ** in forkandexec with path='%s' and args='%s'\n", path, args);
	
	if (pid = fork()) {
		/* in parent, wait for child to complete */
		if (DEBUG) 
			fprintf(stderr, " ** spawned child %i, waiting\n", pid);
		
		wait(&status);
		
		if (DEBUG) 
			fprintf(stderr, " ** child %i exited with code %i\n", pid, status);

	} else {
		/* in child, build the argument list and call execve(...) */
		argv[0] = malloc(strlen(execnm) + 1);
		strcpy(argv[0], execnm);
		
		if (args && strlen(args) > 0) {			
			myargs = malloc(strlen(args) + 1);
			strcpy(myargs, args);
			
			for (arg = strtok(myargs, sep); arg && argn < MAXARGS - 2; arg = strtok(NULL, sep)) {
				argv[argn] = malloc(strlen(arg) + 1);
				strcpy(argv[argn++], arg);
			}
			
			free(myargs);
		}
		
		/* null-terminate the argument list */
		argv[argn] = NULL;
		
		if (DEBUG) 
			for (argn=0; argv[argn]; argn++) 
				fprintf(stderr, " ** arg[%i] = \"%s\"", argn, argv[argn]);
		
		execv(path,argv);
		/* we're done, no need to free() */
	}
}

int main() {
	char path[MAXPATHLEN], errprompt[MAXCMDLEN];
	char *execnm, *args, *command;
	int running = 1;
	
	while (running) {
		/* retrieve a command string and place it in 'command' */
		command = getcmd();
		
		if (command && strlen(command) > 0) {
			/* split command into execnm (command name) and arguments */
			args = command;
			execnm = strsep(&args, " ");
			
			/* check for builtin commands */
			if (strcmp(command, "quit") == 0 || strcmp(command, "exit") == 0) 
				running = 0; 

			else if (strcmp(execnm, "cd") == 0) 
				chdir(args);

			else {
				/* otherwise locate the executable, and if it exists, run it */
				findexec(execnm, path);
				
				if (strlen(path) > 0) {
				
					if (access(path, X_OK) == 0) {
						if (DEBUG) 
							fprintf(stderr, " ** found path: %s\n", path);
							
						forkandexec(path, args);

					} else {
						sprintf(errprompt, "shell: %s", execnm);
						perror(errprompt);
					}

				} else {
					fprintf(stderr, "shell: %s: Command not found.\n", command);
				}
			}
		}
	}
	
	return 0;
}

